What types can be passed over FFI from Rust to Rust?

Inspired by this cool post here, I decided to try writing a macro that would link dynamically in debug mode to allow hotswapping, but link statically in release mode to avoid the overhead/brittleness of dynamic linking.

The hotswapped crate is built as a dylib (not cdylib). This worked surprisingly well! The one issue I found is that I'm getting segfaults when returning both Strings and &'static strs.

My guess is that you can't allocate a String on one side of the FFI boundary and drop it on the other - but I'm not really sure why I can't pass a static str.

Is it documented anywhere which types can and cannot be passed over FFI - specifically with Rust code on both sides, and no C?

1 Like

The one issue I found is that I’m getting segfaults when returning both Strings and &'static strs.

You're probably getting segfaults when passing around a &'static str because that's a reference to some place embedded in the DLL. Normally a string embedded in a DLL will last the lifetime of the program, but when you are hotswapping the &'static str actually only lives as long as the DLL is loaded. So if you use a string from a previous version of your DLL (after hotswapping) you're trying to access data which has been unloaded from memory.

My guess is that you can’t allocate a String on one side of the FFI boundary and drop it on the other - but I’m not really sure why I can’t pass a static str.

I'm not sure why you get segfaults with String. Out of curiosity, what happens when you explicitly tell the dylib to use the system allocator? I'm not sure how, but it could be that your dylib embedded a copy of jemalloc and then the String is somehow trying to deallocate itself from that original allocator... Which won't be in memory any more if you've done any hotswapping.


Another thing to look out for is using trait objects. In the past I tried writing a plugin system which loads a DLL, calls a pre-defined "register" function that returns a trait object (e.g. Box<Plugin>) then after I'd got the plugin I unloaded the DLL (because we don't need to use it any more). I then spent a good 5 or 10 minutes trying to figure out why I'd get a segfault every time I try to call a method on the trait object. Turns out the trait object vtable points to chunks of code in the DLL it came from... Which I'd already unloaded from memory.

So yeah, make sure your DLL outlives any trait objects or 'static types (which aren't really 'static in the usual sense) you may get from it.

2 Likes

Re String, the default dylib allocator is the system allocator and the default binary allocator is jemalloc. If you use one to drop allocations made in the other, you will probably panic.

You can swap the allocators around using nightly, and in my experience it mostly works. This is mostly luck, though, as the Rust folks promise very little about the stability of the binary layout of types between builds. Many systems types (e.g. String) have their layouts locked down, but any new types you create can have different layouts build to build.

If it helps, https://github.com/frankmcsherry/differential-dataflow/blob/master/server/src/bin/server.rs is an example of a computation that loads up dylibs on request and happily passes lots of types allocated on one side back to the other. I suspect it gives Rust core devs conniptions, but Rust-to-Rust FFI seems like it should be a thing that works, even if it isn't spec'd to work yet.

2 Likes

Given C++ still has no standard/stable ABI, what are the chances Rust will see one anytime soon? :slight_smile:

Seems like if you want a plugin system that’s not susceptible to breakage, you’re steered towards IPC (for better or worse) or using a C ABI even for Rust2Rust.

1 Like

There are all really useful answers! I'll try static strings again but be more careful of lifetimes, and I'll try changing allocators as well.

Being restricted to nightly - and even the whole thing being utterly fragile - is probably fine since this is debug only, and all disappears in release mode. Though some robustness for my own sanity would be nice.

Thanks!

1 Like

OK, so I tried returning static strings again while making sure to keep the dylib alive, and I seem to still be getting segfaults/garbage strings. What's more, the string seems to have a different pointer value before and after crossing the FFI boundary:

// In dylib:
#[no_mangle]
pub fn message() -> &'static str
{
    const MESSAGE: &'static str = "Hello, world!";
    println!("Address before: {:?}", MESSAGE.as_ptr());
    MESSAGE
}

// In main crate:
let message = dynamic::message();
println!("Address after: {:?}", message.as_ptr());

// Output:
// Address before: 0x107a65579
// Address after: 0x107592000

Not sure what to make of this exactly.

EDIT: Ignore this - it turned out I was using Symbol<extern fn()> instead of Symbol<fn()>, so my main crate was using the wrong calling convention. (I think?)

1 Like

I got this all working to a pretty decent level and published a crate in case anyone's interested:

https://crates.io/crates/dymod

https://github.com/Pirh/dymod

I also have some ideas on how to handle heap allocations more gracefully that I might try in a while.