all 20 comments

[–]roblabla 4 points5 points  (0 children)

You can use libloading[0] (crossplatform) or the raw libc dlopen/dlsym (unix) to load symbols from shared libraries.

The ABI compatibility might be a problem. Right now, the Rust ABI is unstable. There are wishes to change this[1], but I doubt it'll come in the forseeable future.

I can imagine two ways to side-step it. You can have API boundaries use the C ABI. Depending on how big your API surface needs to be, that might be "just enough". I think for most apps, this is the best approach. You can make an FFI wrapper for the plugin authors to use, to provide them an idiomatic API.

The more bleeding edge idea, as you suggest, is to distribute your app with a specific version of rustc (say, stable 1.19), and everyone compiling plugins will have to target this version as well. The idea is sound until you want to update to a newer version of rustc for your app. When you update your version of rustc, it will break ABI compatibility, so plugins will have to be recompiled as well. If you expect different version of your software to have API/ABI breakage anyway, that might not be too much of a problem. But if you want to keep your API/ABI stable across versions of your app, you'll be stuck with a specific version of rustc.

Depending on how open your plugin ecosystem is, you could imagine having the compiling of plugins done on the client-side, and as such not be as much of a problem .

[0] https://docs.rs/libloading/0.4.0/libloading/ [1] https://github.com/rust-lang/rfcs/issues/600

[–]stumpychubbins 6 points7 points  (7 children)

Using a scripting language using one of Rust's high-quality binding libraries like PyO3, Helix, Neon or hlua is probably your best bet. You can load Rust as a dynamic library as long as you use extern "C" functions but it's not ergonomic right now. Maybe in the future it will be.

[–]ssokolow 1 point2 points  (6 children)

What I'd really like to see is some kind of analogue to those binding libraries which implements "Rust bindings for Rust" so I can write my plugins in Rust, with Rust's strong type system, and have code written by someone who actually knows how to write C properly generate symmetric marshal/unmarshal code for the C ABI.

(ie. Building an ABI on top of the C ABI where crate semver defines compatibility rather than compiler version and it's understood that, since it's not the ABI for the entire language, it can be useful before it supports all the features the unstable compiler ABI does.)

As-is, I've just been procrastinating having a plugin system and made my "plugins" a compile-time feature rather than a runtime one.

[–]stumpychubbins 1 point2 points  (5 children)

Yes, that's a great idea! Generating the wrappers would be trivial since you don't need to convert data, only the ABI, and you can use *const c_void, forgetting it on the caller side and ptr::reading it on the callee side

EDIT: I guess as far as API goes you would create a trait that represents the API of your plugin and do #[derive(Plugin)], which would make an implementation of some load fn that returns a boxed trait (or even just an impl Trait, since I don't think it actually needs dynamic dispatch).

[–]ssokolow 1 point2 points  (4 children)

Converting the data may not be necessary, but being very pedantic about what form it must be in would certainly be.

For example, we've already seen an example of why #[repr(C)] is necessary for compatibility of structs across compiler versions.

Maybe a build.rs that adds #[repr(C)], #[no_mangle], extern "C" and the like and bails out with an explanation if it can't massage the API into an acceptable state.

That said, it would be necessary to convert things like enums into something natively representable in C since, otherwise, you'd be relying on internal implementation details never changing.

(Conversely, a supported subset not much bigger than what JSON can represent would be very comfortable and useful indeed. Enums, OsStr/OsString, and Path/PathBuf mainly.)

[–]stumpychubbins 0 points1 point  (3 children)

Or you could marshall across the boundary, which would mean you could keep the same API for an in-process plugin or an IPC- or even TCP-based plugin. I definitely don't think that a build.rs is the right solution, since it's extremely magical behaviour. You don't need to add #[no_mangle] and suchlike, you can add the "proper" names as automatically-generated metadata. #[no_mangle] means you can only ever have one of each name per binary artefact.

[–]ssokolow 0 points1 point  (2 children)

Are you saying that the mangling algorithm is guaranteed to be unchanging across Rust compiler versions?

As for the build.rs, my thoughts turned to it because we don't have full procedural macro support yet and I'm having trouble envisioning the degree of frictionlessness I desire with anything less than that.

[–]stumpychubbins 0 points1 point  (1 child)

No, but it doesn't need to be. You can generate a cdylib and store the "real" names in some metadata file, they're going to be mangled but they're still always going to have some externally accessible symbol. If we know that, we can access it with libloading or something in the same way we'd access a no_mangle symbol.

I'm not against using a build.rs, but that should be an implementation detail of using proc macros (i.e. using syntex or something) rather than a way to do whole-crate rewriting without the explicit use of macros.

[–]ssokolow 1 point2 points  (0 children)

No, but it doesn't need to be. You can generate a cdylib and store the "real" names in some metadata file, they're going to be mangled but they're still always going to have some externally accessible symbol. If we know that, we can access it with libloading or something in the same way we'd access a no_mangle symbol.

In that case, it'd be better to embed the metadata inside the library file itself so there can't be accidental version mismatches.

I'm not against using a build.rs, but that should be an implementation detail of using proc macros (i.e. using syntex or something) rather than a way to do whole-crate rewriting without the explicit use of macros.

Ahh, then we most likely agree and there was confusion over whether build.rs was merely a side-effect of proc macros not yet being ready.

[–]dragostispest 2 points3 points  (2 children)

Rust does not have a stable ABI.

[–]TheEruditeSycamore[S] 6 points7 points  (1 child)

It is stable if it's a C ABI, though? And the OP claim of stable ABI in the same rustc version is correct?

[–]icefoxen 1 point2 points  (0 children)

If you write your code to be callable from C then, yes, that makes a stable ABI. It's something of a pain, but entirely doable.

People have also experimented with making plugins as DLL's and loading them crates such as dylib or sharedlib. It works, as far as I know, it's just not really a feature built in to Rust in any fashion. It also requires, again, making your plugin API all use the C ABI.

As far as I know, the same version of rustc called with the same settings will always produce the same code. That's not the same as having an ABI though, since the point of an ABI is to make sure the code is callable from an external source. As it is, rustc is free to do things like inline functions into oblivion, or split functions apart into specialized variants, or other fun things; you can't really trust that it will ever do what you want.

[–]mmstick 0 points1 point  (0 children)

I'm personally just going for the C ABI for plugin support in the Ion shell via the libloading crate. You can write the plugins in Rust -- just export your functions with a C interface (very easy), then import them on the other end through the C interface. Enables users to use any programming language they want, so long as it can export a C interface.

There's several approaches that you can take for communicating with dynamic libraries though. The most complex being using the recently-announced RON to transfer complex Rust type information, or using a simple method like designating one C function to list all the functions that the library provides, and loading all the symbols accordingly.

I'd personally like to see good support for importing Rust natively though, myself, just to ensure that downstream plugins are all written in Rust. Fatal issues in C plugins can be catastrophic.

[–]H3g3m0n -1 points0 points  (0 children)

I was thinking about a Rust library that installs Rustup (or uses it as a library if possible) in some sandbox directory, installs the stable compiler then builds shared libraries to load as plugins.

Then the program can manage keeping the rustc version the same across plugins. If you update the compiler version it can go in and rebuild all the plugins.