Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward Qt logging to Rust log!() facade #86

Merged
merged 12 commits into from
May 2, 2020
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct Greeter {
}

fn main() {
qmetaobject::log::init_qt_to_rust();
qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
let mut engine = QmlEngine::new();
engine.load_data(r#"import QtQuick 2.6; import QtQuick.Window 2.0;
Expand All @@ -58,9 +59,33 @@ Window {

Requires Qt >= 5.8

## Cargo features

Cargo provides a way to enable (or disable default) optional [features](https://doc.rust-lang.org/cargo/reference/features.html).

### `log`

By default, Qt's logging system is not initialized, and messages from e.g. QML's `console.log` don't go anywhere.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't they go in the stderr by default?

I mean, they follow the default Qt handler which is also quite configurable with env variable: https://doc.qt.io/qt-5/debug.html#warning-and-debugging-messages / https://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories

Copy link
Collaborator Author

@ratijas ratijas May 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk, by default they just not showing up at all on my system. I haven't been able to figure out why, but now, after manual activation, it works, which is fine for me.

The "log" feature enables integration with [`log`](https://crates.io/crates/log) crate, the Rust logging facade.

The feature is enabled by default. To activate it, execute the following code as early as possible in `main()`:

```rust
fn main() {
qmetaobject::log::init_qt_to_rust();
// don't forget to set up env_logger or any other logging backend.
}
```

### `chrono_qdatetime`

Enables interoperability of `QDate` and `QTime` with Rust [`chrono`](https://crates.io/crates/chrono) package.

This feature is disabled by default.

## What if a binding for the Qt C++ API you want to use is missing?

It is quite likely that you would like to call a particular Qt function wich is not wrapped by
It is quite likely that you would like to call a particular Qt function which is not wrapped by
this crate.

In this case, it is always possible to access C++ directly from your rust code using the cpp! macro.
Expand Down
3 changes: 2 additions & 1 deletion qmetaobject/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ keywords = ["Qt", "QML", "QMetaObject",]
repository = "https://github.com/woboq/qmetaobject-rs"

[features]
default = ["log"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'm just concerned about having too many dependencies. That's also why i wouldn't put it as the default either.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think debugging is essential, especially on such multi-lingua projects, and should be on by default. By having it on by default AND providing instructions for activation on the front-page example we are lowering the barrier for newcomers.

chrono_qdatetime = ["chrono"]

[dependencies]
qmetaobject_impl = { path = "../qmetaobject_impl", version = "=0.1.4"}
lazy_static = "1.0"
cpp = "0.5.4"
chrono = { version = "0.4", optional = true }
log = { version = "0.4", optional = true }

[build-dependencies]
cpp_build = "0.5.4"
Expand All @@ -28,7 +30,6 @@ cstr = "0.1"
if_rust_version = "1"
tempfile = "^3"


[package.metadata.docs.rs]
dependencies = [ "qtbase5-dev", "qtdeclarative5-dev" ]
rustc-args = [ "--cfg feature=\"docs-only\"" ]
66 changes: 2 additions & 64 deletions qmetaobject/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,70 +814,6 @@ fn add_to_hash(hash: *mut c_void, key: i32, value: QByteArray) {
/// Refer to the documentation of Qt::UserRole
pub const USER_ROLE: i32 = 0x0100;

cpp_class!(
/// Wrapper for Qt's QMessageLogContext
pub unsafe struct QMessageLogContext as "QMessageLogContext"
);
impl QMessageLogContext {
// Return QMessageLogContext::line
pub fn line(&self) -> i32 {
cpp!(unsafe [self as "QMessageLogContext*"] -> i32 as "int" { return self->line; })
}
// Return QMessageLogContext::file
pub fn file(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->file;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}
// Return QMessageLogContext::function
pub fn function(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->function;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}
// Return QMessageLogContext::category
pub fn category(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->category;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}
}

/// Wrap Qt's QtMsgType enum
#[repr(C)]
#[derive(Debug)]
pub enum QtMsgType {
QtDebugMsg,
QtWarningMsg,
QtCriticalMsg,
QtFatalMsg,
QtInfoMsg,
}

/// Wrap qt's qInstallMessageHandler.
/// Useful in order to forward the log to a rust logging framework
pub fn install_message_handler(logger: extern "C" fn(QtMsgType, &QMessageLogContext, &QString)) {
cpp!(unsafe [logger as "QtMessageHandler"] { qInstallMessageHandler(logger); })
}

/// Embed files and made them available to the Qt resource system.
///
/// The macro accepts an identifier with optional preceding visibility modifier,
Expand Down Expand Up @@ -1022,6 +958,8 @@ pub mod itemmodel;
pub use itemmodel::*;
pub mod listmodel;
pub use listmodel::*;
pub mod log;
pub use crate::log::*;
pub mod qtdeclarative;
pub use qtdeclarative::*;
pub mod qmetatype;
Expand Down
230 changes: 230 additions & 0 deletions qmetaobject/src/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! Logging facilities and forwarding

use std::os::raw::c_char;

#[cfg(feature = "log")]
use log::{Level, logger, Record};

use crate::QString;

cpp! {{
#include <qmetaobject_rust.hpp>
}}

cpp_class!(
/// Wrapper for [`QMessageLogContext`] class.
///
/// [`QMessageLogContext`]: https://doc.qt.io/qt-5/qmessagelogcontext.html
pub unsafe struct QMessageLogContext as "QMessageLogContext"
);

impl QMessageLogContext {
/// Wrapper for `QMessageLogContext::line`.
pub fn line(&self) -> i32 {
cpp!(unsafe [self as "QMessageLogContext*"] -> i32 as "int" { return self->line; })
}

/// Wrapper for `QMessageLogContext::file`.
pub fn file(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->file;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}

/// Wrapper for `QMessageLogContext::function`.
pub fn function(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->function;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}

/// Wrapper for `QMessageLogContext::category`.
pub fn category(&self) -> &str {
unsafe {
let x = cpp!([self as "QMessageLogContext*"] -> *const c_char as "const char*" {
return self->category;
});
if x.is_null() {
return "";
}
std::ffi::CStr::from_ptr(x).to_str().unwrap()
}
}
}

/// Wrapper for [`Qt::QtMsgType`][] enum.
///
/// [`Qt::QtMsgType`]: https://doc.qt.io/qt-5/qtglobal.html#QtMsgType-enum
#[repr(C)]
// XXX: Do NOT derive Ord and PartialOrd.
// XXX: Variants are not ordered by severity.
// XXX: Also, levels ordering is not implemented in Qt, only == equality.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum QtMsgType {
QtDebugMsg,
QtWarningMsg,
QtCriticalMsg,
QtFatalMsg,
QtInfoMsg,
// there is also one level defined in C++ code:
// QtSystemMsg = QtCriticalMsg
}

#[cfg(feature = "log")]
impl From<QtMsgType> for Level {
/// Mapping from Qt logging levels to Rust logging facade's levels.
///
/// Due to the limited range of levels from both sides,
/// [`QtCriticalMsg`][`Qt::QtMsgType`] and [`QtFatalMsg`][`Qt::QtMsgType`]
/// both map to [`log::Level::Error`][Level],
/// while [`log::Level::Trace`][Level] is never returned.
///
/// [Level]: https://docs.rs/log/0.4.10/log/enum.Level.html
/// [`Qt::QtMsgType`]: https://doc.qt.io/qt-5/qtglobal.html#QtMsgType-enum
fn from(lvl: QtMsgType) -> Self {
match lvl {
QtMsgType::QtDebugMsg => Level::Debug,
QtMsgType::QtInfoMsg => Level::Info,
QtMsgType::QtWarningMsg => Level::Warn,
QtMsgType::QtCriticalMsg => Level::Error,
QtMsgType::QtFatalMsg => Level::Error,
// XXX: What are the external guarantees about possible values of QtMsgType?
// XXX: Are they promised to be limited to the valid enum variants?
}
}
}

#[cfg(feature = "log")]
impl From<Level> for QtMsgType {
/// Mapping back from Rust logging facade's levels to Qt logging levels.
///
/// Not used internally, exists just for completeness of API.
///
/// Due to the limited range of levels from both sides,
/// [`log::Level::Debug`][Level] and [`log::Level::Trace`][Level]
/// both map to [`QtDebugMsg`][`Qt::QtMsgType`],
/// while [`QtFatalMsg`][`Qt::QtMsgType`] is never returned.
///
/// [Level]: https://docs.rs/log/0.4.10/log/enum.Level.html
/// [`Qt::QtMsgType`]: https://doc.qt.io/qt-5/qtglobal.html#QtMsgType-enum
fn from(lvl: Level) -> Self {
match lvl {
Level::Error => QtMsgType::QtCriticalMsg,
Level::Warn => QtMsgType::QtWarningMsg,
Level::Info => QtMsgType::QtInfoMsg,
Level::Debug => QtMsgType::QtDebugMsg,
Level::Trace => QtMsgType::QtDebugMsg,
}
}
}

/// Wrapper for [`QtMessageHandler`][] typedef.
///
/// [`QtMessageHandler`]: https://doc.qt.io/qt-5/qtglobal.html#QtMessageHandler-typedef
pub type QtMessageHandler = Option<extern "C" fn(QtMsgType, &QMessageLogContext, &QString)>;

/// Wrapper for [`qInstallMessageHandler`] function.
///
/// # Wrapper-specific behavior
///
/// To restore the message handler, call `install_message_handler(None)`.
///
/// [`qInstallMessageHandler`]: https://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler
pub fn install_message_handler(logger: QtMessageHandler) -> QtMessageHandler {
cpp!(unsafe [logger as "QtMessageHandler"] -> QtMessageHandler as "QtMessageHandler" {
return qInstallMessageHandler(logger);
})
}

// Logging middleware, pass-though, or just proxy function.
// It is called from Qt code, then it converts Qt logging data
// into Rust logging facade's log::Record object, and sends it
// to the currently active logger.
#[cfg(feature = "log")]
extern "C" fn log_capture(msg_type: QtMsgType,
context: &QMessageLogContext,
message: &QString) {
let level = msg_type.into();
let target = match context.category() {
"" => "default",
x => x,
};
let file = match context.file() {
"" => None,
x => Some(x),
};
let line = match context.line() {
// In Qt, line numbers start from 1, while 0 is just a placeholder
0 => None,
x => Some(x as _),
};
let mut record = Record::builder();
record.level(level)
.target(target)
.file(file)
.line(line)
.module_path(None);
// (inner) match with single all-capturing arm is a hack that allows us
// to extend the lifetime of a matched object for "a little longer".
// Basically, it retains bounded temporary values together with their
// intermediate values etc. This is also the way how println! macro works.
match context.function() {
"" => match format_args!("{}", message) {
args => logger().log(&record.args(args).build()),
},
f => match format_args!("[in {}] {}", f, message) {
args => logger().log(&record.args(args).build()),
}
}
}

/// Installs into [Qt logging system][qt-log] a function which forwards messages to the
/// [Rust logging facade][log].
///
/// Most metadata from Qt logging context is retained and passed to [`log::Record`][].
/// Logging levels are mapped with the default [`From`][lvl] implementation.
///
/// This function may be called more than once.
///
/// [qt-log]: https://doc.qt.io/qt-5/qtglobal.html#qInstallMessageHandler
/// [log]: https://crates.io/crates/log
/// [`log::Record`]: https://docs.rs/log/0.4.10/log/struct.Record.html
/// [lvl]: ./struct.QtMsgType.html
#[cfg(feature = "log")]
pub fn init_qt_to_rust() {
// The reason it is named so complex instead of simple `init` is that
// such descriptive name is future-proof. Consider if someone someday
// would want to implement the opposite forwarding logger?
install_message_handler(Some(log_capture));
}

#[cfg(test)]
#[cfg(feature = "log")]
mod tests {
use super::*;

#[test]
fn test_double_init() {
// must not crash
init_qt_to_rust();
init_qt_to_rust();
}

#[test]
fn test_convert() {
assert_eq!(Level::from(QtMsgType::QtInfoMsg), Level::Info);
assert_eq!(QtMsgType::QtCriticalMsg, QtMsgType::from(Level::Error))
}
}
Loading