Really interesting corollary: because 'crate <: 'static, you can't use Any for / get the TypeId of the types defined within a "plugin crate." This is interesting because TypeId's probabilistic unsoundness is only unsound in the face of dynamic linking outside of Cargo's purview; within every single cargo graph of compilation, the compiler (supposedly, I didn't fully verify whether it's actually cross-crate) verifies that all TypeIds are properly unique.
So, even without switching away from probabilistic TypeId, the soundness hole could be argued to be irrelevant if we only permit non-Cargo linking of Rust binary artifact crates if all but one of them are plugin artifacts or only have an entirely C-compatible API.
Well, if you can still get 'static-bound stuff, not just preserve 'static in generic lifetimes, that's a bit bust, and you still also aren't preventing plugin-spawned unscoped threads unless you also modify std.
The idea is that within the plugin you can't get 'static things unless they're leaked allocations or passed in by the host program. so I don't think the 'static bounds can cause problems...
one thing that would be required is that any types (including lambda functions) defined in the plugin would need to not : 'static since all their methods are bounded by 'crate, not 'static.
well, which API are you thinking the plugin would use? remember std::thread::spawn requires the passed in function to have a 'static bound, which none of the functions in the plugin do. scoped threads joins all threads before exiting the scope, which must happen before the plugin's lifetime expires since you're running code in the plugin. so the borrow checker would prevent trying to unload the plugin before the plugin's scoped threads stop running...
And I can't think of any reason why a crate couldn't provide generic "plugin host" functionality, with the language as it is today. (I get really nervous about generic RPC in general because I've seen that turn into Hyrum's Law catastrophes, but that wouldn't be the crate's problem.)
I feel like it ought to be possible to do out-of-process plugins efficiently enough for audio with shared memory, but I've never tried to write that kind of code and I could believe the context switch overhead by itself is prohibitive, especially if you have a long chain of effects plugins. (The JACK people say "this is possible but it does not scale".)
Everything's harder in kernel space anyway, be prepared to debug hard
I observe that reloading a chunk of code is an easier task than unloading it, because the code still exists afterward. "All" you have to do is stop the world and fix up all pointers to the old module image to refer to the matching locations in the new module image. That process will no doubt require unsafe code, but it ought to be encapsulated unsafety with no external impact on lifetimes, and I have the impression the Linux kernel's hot-patching infrastructure is already capable of something not entirely unlike it. (I have never done more than trivial work on Linux-the-kernel.)
Not how it is implemented in the kernel. You need to unload and then load. There is no atomic reload support for modules.
The hot patch support is not suitable for writing drivers really (at least not the ACPI driver I was working on). I have never tried it myself, but my understanding is that you make a special (and carefully crafted) kernel module that when loaded will patch existing code in place. It is not usable for the quick development and test cycle of making a driver.
It would be interesting if all functions/statics were bound by a lifetime of their "global" scope, that just happens to be 'static in normal use, but somehow could be explicitly borrowed for a shorter time. &'crate fn() could only get a &'crate GLOBAL reference, as if it was borrowing from the same global object the function is from.
In such scenario a dl-opened function would have its "global" context borrowed, and couldn't use thread::spawn with a closure that references its crate's symbols, because the other functions would be &'crate fn(), and make the closure Fn() + 'crate, and not meet thread::spawn's requirement of a real 'static.
Thus the need to stop the world and fix up the old pointers, yes.
So, in the Rust shim layer, you stop the world (using the process freezer, perhaps), unload, load, fix all the dangling references, resume. Simple matter of programming
I thought there was a code patching mechanism in Linux that didn't involve loading an entire module to do it. Something to do with optimizing conditional branches on sysctl values and other such tuning knobs that almost never change. Can't remember what it was called.
Ah yes, that would be static keys. Which is all about changing branches rarely (e.g. most trace points are almost never active). You have a block of NOPs that can when needed get replaced with a jump. For module development I don't think it would work well, since you would still need to load the new code to replace the old code, then patch up the old code to call the new code instead. What happens on the third or fourth iteration though? And every time the module you load would need a new name (pretty sure module names must be unique).
Maybe doable, probably not ergonomic, and I don't know that it is actually any safer (you still need to ensure structs haven't changed layouts for example).
My intuition is that audio processing should be lots of data and few context switches (potentially only blocking when they fill/empty buffers), but perhaps in a DAW they do end up needing enough feedback or low enough latency (1ms per stage still adds up to a total latency quick and is pretty heavy on just waiting on buffers!)
For live audio, latency becomes critical; roughly speaking, 3 ms of latency is the same as moving the loudspeaker back by 1 meter, and so there comes a point where the latency is problematic. As an example, if you're trying to present a vocalist with their processed vocals in time with a backing track that's accompanying them, and they're used to stage monitors placed 3 meters away from them, you can probably get away with 10 ms of total latency to their headphones, but no more than that.
It was not my intention to start a discussion of exactly how hard it would be to implement stop-the-world-and-fix-all-the-references kernel module reload in the Linux kernel specifically. I just wanted to make the point that reloading (or hot-patching) code that's been dynamically loaded into some program's address space is a different problem than unloading that code without replacement, and, in particular, that it could in principle be implemented by a program written (partially or completely) in Rust, with encapsulated unsafety, and without adding lifetime tracking for function references to the language.
I don't think this was mentioned in this thread, but one of big annoyances with the current function pointer/reference syntax is that it does not allow easy casting between function pointers and references. The only way to cast between *const c_void and fn (A) -> T is to use transmute! This also means that we can not easily store function pointers in AtomicPtr<FnType> and other "pointer" types, currently we have to use AtomicPtr<c_void> and perform a potentially error-prone transmute dance at call sites.
So, yeah, I would love for the function pointer syntax to be reworked in a next edition.
What about those weird targets where you have different address spaces? AVR etc, but also WASM as I understand it? (see e.g. Maybe Rust should have function references, too - #4 by binarycat). Would your suggestion allow a good way to handle that? Of course, dlsym etc can never work on such a platform, as it returns a *c_voiddata pointer that you need to cast, but that is just typical POSIX.
Ideally, compiler should return a compilation error for casts between different address spaces. But my main point was that right now we have to use *const c_void ponters instead of properly typed *const FnType, i.e. ideally we should not use the former for function pointers in the first place or cast it immediately into a typed pointer if we work with functions like dlsym.
Yeah I just tripped over this myself. Suppose an FFI interface where library code calls into application code, supplying, in C terms, a callback pointer that may be NULL. You're supposed to call it at an appropriate point but only if it's not NULL.
void called_from_library(void (*callback)(void) /* , other arguments */)
{
// ...
if (callback) callback();
// ...
}
The most natural Rust implementation would use Option<fn()> as the callback argument:
fn called_from_library(callback: Option<fn()>, /* other arguments */) {
// ...
if let Some(callback) = callback { callback(); }
// ...
}
And that's perfectly fine if the library is also implemented in Rust. But if the library is C, then, as I read it, the null pointer optimization guarantee for Option does not quite promise that an Option<extern "C" fn()> parameter declaration is compatible with a C caller supplying a void (*)(void) actual argument. I think it's probably meant to, but there's a missing piece: for neither function nor object pointers does it actually say that transmute::<Option<&T>>(ptr::null<T>()) is guaranteed to be valid and to produce None, nor the equivalent in the opposite direction.
Given that, what I wound up writing instead was
#[no_mangle] unsafe extern "C"
fn called_from_library(callback_raw: *const ffi::c_void, /* other arguments */) {
// ...
if let Some(callback_ref) = callback_raw.as_ref() {
let callback_fn: extern "C" fn() = mem::transmute(callback_ref);
callback_fn()
}
// ...
}
I believe this would be sound even if void (*)(void) and Option<extern "C" fn()> weren't transmute-compatible (as long as extern "C" fn() and &c_voidare transmute-compatible, of course). But it sure would be nice to be able to write something more like the all-Rust version. And having some kind of "raw function pointer" type in the language would help.
This is wholly independent of the earlier discussion of giving some chunks of machine code a lifetime shorter than 'static, though.
actually, due to how keywords work, we could just introduce 'crate as a new globally-scoped lifetime.
this would be identical to 'static in most cases, except for in crates that are marked as #![unloadable], which have their constants, functions, and statics have their lifetime demoted to 'crate.
crates marked as unloadable can only depend on other crates that are marked as unloadable. this is pretty analogous to how no_std crates work.
I was going to say I think you got the logic backwards and it should be named #![loadable], except that the issue arises when unloading, so I think unloadable is not the best name due to confusing ability to be unloaded with lack of ability to be loaded. maybe #![can_be_unloaded]?
what about #![dyn_unload]? can_be_unloaded is descriptive, but it might be annoying to talk about, compare "unloadable crates" with "crates that can be unloaded".
Would the compiler enforce any rules around the usage dyn_unload? For example, using thread-local storage in the object blocks unloading on macOS. Should rustc enforce no-TLS in such a crate? How does it tell the linker "error if you pull in a TLS usage from a linked static library"? Does it even try? One must also not leak any raw pointers which probably aligns well with escape analysis being done for other efforts. The musl library just doesn't implement it (though I think this means one can claim "unload" support there unconditionally).
I don't know if one can make it safe enough to fit with Rust's goal of a "pit of success". I'm here to ask hard questions so that if it does appear, it is as robust as possible…I'm just less hopeful of the outcome.
Perhaps another way would be to write a custom module loader (as a crate, at least to begin with) that simply side steps how underlying C library loads things. This is as I understand essentially what happens with loadable modules in for example the Linux kernel (and if Windows supports loading/unloading drivers, probably there as well).
I don't know how workable that would be. It would probably bring an entirely different set of headaches instead, and might not be possible at all on some platforms.