-
Notifications
You must be signed in to change notification settings - Fork 258
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
how to use within dynamic libraries ? #421
Comments
Hi @XopheD 👋 Are you happy to upload your sample as a GitHub Gist or public repository? Are you initializing your app with a call to |
Since the question was about using Also, I packed two crates into a workspace, so it can be built and run with a single cargo command. Now, because they are members of the same workspace, we can replace complex relative paths with a simple let exe = std::env::current_exe().unwrap();
#[cfg(any(target_os = "unix", target_os = "linux"))]
let path = "libdynlib.so";
#[cfg(target_os = "macos")]
let path = "libdynlib.dylib";
let lib_full_path = exe.parent().expect("executable must be in a parent directory").join(path); dynlog - stripped but still doesn't work.zip (update: repository link) Alter all that trouble, we can verify, that logging is indeed does not work in cdylib:
This line in // this is not printed... unfortunately
log::info!("log is working... yes !!"); There's a good reason for that. They are being compiled absolutely separately, with very little in common. In fact, compiler pulls libraries and generates duplicate code for all types, functions and other stuff. Thus, Take a look here: Lines 347 to 367 in 9d42067
If these variables were global/exported/no mangle, only then it would be possible to share runtime logging preferences among shared and dlopen-loaded libraries. But then it would've created various compatibility issues between different versions of Note that such problems do not exist e.g. in Java world, where everything runs on the same instance of JVM, and a mere idea of separately loading packages with all their dependencies into their own memory and address space is absurd. Unless you manage to spin off a second JVM instance (perhaps a different implementation, since VMs often do use global variables). In which case we would be back at square one: not only logging, but any objects would be impossible to pass back and forth without marshaling through JNI (Java Native Interface) glue. So, unless you are willing to do some weird unsound ffi, it is totally up to a you as a developer to ensure consistent logging in Rust The best you could do, it to add a normal Rust crate to your workspace, which contains all logging initialization. Add this crate as a dependency to each impl QQmlExtensionPlugin for QExampleQmlPlugin {
fn register_types(&mut self, uri: &std::ffi::CStr) {
my_shared_logging::init().unwrap(); // <- added initialization
qml_register_type::<TimeModel>(uri, 1, 0, cstr!("Time"));
}
} Once final thought. We could go one layer deeper by factoring out common logging settings in an actual dcylib, while each plugin and exe would merely forward logging requests through well-defined FFI. Some research has been done in this direction already. You gonna need:
Now binary and plugins only need to dynamically (but not via dlopen) link to the shared logging cdylib. All the logic was extracted from them, and only forwarding stubs remain. Shared lib is, on the other hand, well, shared — along with its log preferences — once per process. The only trick is to design FFI-compatible log forwarder. Let's try it out! With a project tree as follow:
Main app executes, sets up log forwarder for itself (which in turn sets up log receiver in "my-cdylib-log"), and then loads "dynlib" which also sets up log forwarder instance in its own memory space. Both forwarders are proxying messages through stable FFI thanks to "ffi-log" types and functions declarations. ❯ cargo build
❯ cd target/debug
❯ LD_LIBRARY_PATH=$(pwd) ./main
INFO [my_cdylib_log] Initialized cdylib
INFO [main] Hello, world!
INFO [main] dyn lib is loaded
INFO [main] f() is loaded
TRACE [dynlib] initialized logging in dynlib
f() is called...
INFO [dynlib] log is working... yes !!
INFO [main] all is done... dynlog - just works.zip (update: repository link) As a bonus, valgrind didn't detect any memory leaks. I've also done some FFI forwarding myself between Rust and Qt in qmetaobject-rs crate. Feel free to check it out :) PS Actually, it was discussed in #66. |
If my research and code implementation answers the original question, I'd like ask either OP or @rust-lang team — whoever comes first — to close this issue. 😶 I posted the code in my repository for easier browsing experience, and added links to my comment above: |
If your application and dll are both written in Rust and compiled by the same compiler, you may use the following code. Just put this file in your application project. use log::{LevelFilter, Log, Metadata, Record};
#[repr(C)]
pub struct LogParam {
pub enabled: extern "C" fn(&Metadata) -> bool,
pub log: extern "C" fn(&Record),
pub flush: extern "C" fn(),
pub level: LevelFilter,
}
struct DLog;
static mut PARAM: Option<LogParam> = None;
pub fn init(param: LogParam) {
let level = param.level;
unsafe {
if PARAM.is_some() {
eprint!("log should only init once");
return;
}
PARAM.replace(param);
}
if let Err(err) = log::set_logger(&LOGGER).map(|_| log::set_max_level(level)) {
eprint!("set logger failed:{}", err);
}
}
fn param() -> &'static LogParam {
unsafe { PARAM.as_ref().unwrap() }
}
impl Log for DLog {
fn enabled(&self, metadata: &Metadata) -> bool {
(param().enabled)(metadata)
}
fn log(&self, record: &Record) {
(param().log)(record)
}
fn flush(&self) {
(param().flush)()
}
}
static LOGGER: DLog = DLog;
#[no_mangle]
extern "C" fn enabled(meta: &Metadata) -> bool {
log::logger().enabled(meta)
}
#[no_mangle]
extern "C" fn log(record: &Record) {
log::logger().log(record)
}
#[no_mangle]
extern "C" fn flush() {
log::logger().flush()
}
pub fn log_param() -> LogParam {
LogParam {
enabled,
log,
flush,
level: log::max_level(),
}
} And then write a function like this in your dll project to be called in the application. #[no_mangle]
extern "C" fn init_logger(param: LogParam) {
init(param);
} When the application opens the dll, just call the init_logger in the application domain like this init_logger(log_param()); log_param function called in the application, so the function points to the application function. |
@lazytiger please, don't advice that. Read my post carefully, and then edit/remove your post. It is very wrong for several reasons.
|
@ratijas Thanks for the reply. 1、If application and dll are both written in Rust, and compiled by the same compiler, it's ok, because Metadata is passed by reference, ie pointer, they are the same layout in memory. 2、Mutable global only used in initialize, so it's ok either |
I have a perfect working solution, much simpler than above. How to use
IdeaThe idea is to forward use log::Metadata;
#[repr(C)]
pub struct SharedLogger {
formatter: for<'a> extern "C" fn(&'a log::Record<'_>),
}
impl log::Log for SharedLogger {
fn enabled(&self, _: &Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
(self.formatter)(record)
}
fn flush(&self) {}
}
pub fn build_shared_logger() -> SharedLogger {
extern "C" fn formatter(r: &log::Record<'_>) {
tracing_log::format_trace(r).unwrap()
}
SharedLogger { formatter }
}
#[no_mangle]
pub extern "C" fn setup_shared_logger(logger: SharedLogger) {
if let Err(err) = log::set_boxed_logger(Box::new(logger)) {
log::warn!("{}", err)
}
}
|
Thanks for investigating this @ratijas and @qiujiangkun! I've added a note to the readme about dynamic libraries that links back here and suggests some FFI-safe wrapper. |
I find a simple way. Crate In dynamic libraries file, define a method to initiate logger:
In main program, get current logger object value and pass to dynamic library:
This should be called after logger object inited in main program. |
I have been going a slightly different but closely related use case to describe above: I have one query that I don't quite understand: I followed on down the codebase and found this as the pattern to help solve the issue https://github.com/ratijas/rustc-issues/blob/dynlog-works/ffi-log/src/lib.rs#L195 . However, I don't quite understand what this is doing and why this is necessary to write it with this structure rather than returning the Record object that can be sent direct to the logging function. Would you be able to provide some commentary to aid my learning. My equivalent to the code is here. https://github.com/Bengreen/rusty-microservice/blob/make-so/ffi-log2/src/lib.rs#L242 |
Hi, @Bengreen Thanks for the trip down the memory line. I can barely remember what it was all about about, but let's not waste any time.
Here, we are talking about a method on impl ExternCRecord {
pub unsafe fn as_record(&self) -> RecordBuilder {/*...*/} while #[repr(C)]
pub struct ExternCRecord {
pub metadata: ExternCMetadata,
/// fmt::Arguments<'a> are not FFI-safe, so we have no option but to format them beforehand.
pub message: RustString,
pub module_path: RustStr, // None points to null
pub file: RustStr, // None points to null
pub line: i64, // None maps to -1, everything else should fit in u32.
} See the comment on pub extern "C" fn rust_log_log(record: &ExternCRecord) {
let mut builder = unsafe { record.as_record() };
match format_args!("{}", unsafe { record.message.to_str() }) {
args => {
let record = builder.args(args).build();
log::logger().log(&record);
}
}
} If you inspect the code a bit more, you'll see the uses of And that's why I'm not a big fan of the other "lazy" approaches posted in this thread. Compiler just does not guarantee that any non-repr(C) structures will be compatible on both ends. You gotta take care, and manually expose yourself literally everything via FFI. |
* Add new variant `BinstallError::DuplicateSourceFilePath` * Check `bin.source` in `collect_bin_files` * Add new variant `BinstallError::InvalidSourceFilePath` * Make sure generated source_file_path cannot access outside curdir * Add new variant `BinstallError::EmptySourceFilePath` * Ensure source_file_path is not empty Signed-off-by: Jiahao XU <[email protected]>
For reference, a more elegant and concise shared logger for tracing |
How does that crate solve the use case of usage in dynamic libraries? |
Hi,
I develop an application based on dynamic loading. I don’t understand how to do to display log messages generated by the loaded modules. I misunderstood probably stg.
To play with the joined example :
The main program loads the dynamic library libdynlib and launches f(), the loaded function.
Unfortunately, the log::info! message inside f() is not displayed.
Rusty regards,
dynlog.zip
The text was updated successfully, but these errors were encountered: