Hacker Newsnew | past | comments | ask | show | jobs | submit | abendstolz's commentslogin

Don't rust shared libraries have the problem of no stable rust ABI? So you either use the C ABI or you use some crate to create a stable rust ABI, because otherwise a shared lib compiled with rust compiler 1.x.y on system a isn't guaranteed to work with the binary compiled on the same system with another compiler... or on another system with the same compiler version.

Right?


You'd have a plugin layer that hooks in the right places with a stable ABI on one end, and a native feeling interface on the other.


Very cool!

But I prefer the wasmtime webassembly component model approach these days.

Built a plugin system with that, which has one major upside in my book:

No stringly function invocation.

Instead of run_function("my-function-with-typo") I have a instantiated_plugin.my_function call, where I can be sure that if the plugin has been instantiated, it does have that function.


This sounds like a good approach to overcoming rusts slow compile times and lack of dynamic linking. One thing I'm concerned about with this path is what about hot reloading and fast script running? Doesn't everything in the wasm component model need to be compiled first? I imagine that would remove some of the advantages to using a scripting language like JavaScript or Python.


You're right. Hot reloading isn't done by default.

I manually compile a plugin and in my system I can "refresh" a plugin and even say "activate version 1.1 of the plugin" or "activate version 1.2" of the plugin etc.

But that's something I had to build myself and is not built into wasmtime itself.


Alternatively, whenever designing a scripting/plugin host make sure to support plugin-hosting-plugins. That way you could have the Roto plugin host complied to wasm.


Sounds interesting but at the same time a bit complex.

I assume you wouldn't ship the whole plugin runtime for each plugin that wants to host another plugin?!


It's not that complex. You basically want to have all plugins support describing themselves (as a list of plugins, which is nearly always 1 item), and "activating" themselves according to one of their descriptors. Bonus points for deactivation for updates (this is often extremely hard to pull off because of how instrusive plugins can be). You could then have a utility header file or whatnot that does the [very minor] plumbing to make the plugin API behave like a single plugin.

Shipping the runtime is another good option, because it means you don't have to worry about runtime versioning.


How is that not also stringly typed?


                match Plugin::instantiate_async(&mut store, &component, &linker).await {
                    Ok(plugin) => {
                        match plugin
                            .plugin_guest_oncallback()
                            .call_ontimedcallback(&mut store, &callback_name)
                            .await
                        {
                            Ok(()) => debug!("Successfully called oncallback for {plugin_path:?}"),
                            Err(e) => warn!("Failed to call oncallback for {plugin_path:?}: {e}"),
                        }
                    }
                    Err(e) => {
                        error!("Failed to call oncallback for {plugin_path:?}!: {e}");
                    }
                }
See the "call_ontimedcallback"? It's not a string. The compiler ensures it exists on the Plugin type generated from the .wit file.

If of course I put a wasm file in the plugin folder that doesn't adhere to that definition, that wasm file isn't considered a plugin.


Thanks for using wasmtime! I worked on the component bindings generator you’re using and it’s really nice to see it out in the wild.

To elaborate a bit further: wasmtime ships a [bindings generator proc macro](https://docs.wasmtime.dev/api/wasmtime/component/macro.bindg...) that takes a wit and emits all the code wasmtime requires to load a component and use it through those wit interfaces. It doesn’t just check the loaded component for the string names present: it also type checks that all of the types in the component match those given by the wit. So, when you call the export functions above, you can be quite sure all of the bindings for their arguments, and any functions and types they import, all match up to Rust types. And your component can be implemented in any language!


Thanks so much for your work on this! So far it's been a good pattern for adding hot-reloadable logic to my Rust projects.

Next up I'm hoping to integrate this stuff into a game so you can have hot-reloadable game logic, with the potential for loading and sandboxing user-created mods.


Would wasmtime be a good general purpose scripting extension layer for a C program in the style of embedded Lua but faster and more generic? Is that a reasonable use-case?


Wasmtimes C bindings for components are still a work in progress. In general C programming is always going to require more work than Rust programming, due to the limitations of C abstractions, but at the moment it’s also held back by how much energy has gone into the C bindings, which is much less than the Rust bindings which many of the maintainers, myself included, created and are using in production at their day jobs.

More info: https://github.com/bytecodealliance/wasmtime/issues/8036 Several folks have volunteered work on the C bindings over the last year or two, with this contributor making progress most recently: https://github.com/bytecodealliance/wasmtime/pulls?q=is%3Apr...


Ah, fair enough. So it is still stringly typed, it's just verified at compile time. Which I guess is true about all compiled functions ever.


For binaries, I agree. For libs, I prefer semver



Thank you!


Can you elaborate as to why "his isn't how you'd do things in userspace, but, this isn't userspace so fine" holds?

Naive me - not a kernel dev at all - would argue that returning Result<Memory, AllocationError> is always better, even for userspace because it would allow me to additionally log something or gracefully deal with this.

Even if I don't want to deal with it, I could just `.unwrap()` or `.expect('my error message')` it.

Note: I am not trying to be snarky here, I genuinely don't know and would like to.

If answering this is too complex, maybe you can point me in the right direction so I can ask the right questions to find answers myself? Thanks in any case!


> it would allow me to additionally log something

If you don't have any memory your allocations are all failing. When you assemble the log message, the allocation needed to do that fails. Bang, double fault.

Now, often people don't really mean they want allocations to be able to fail generally, they're just thinking about that code they wrote that reads an entire file into RAM. If it was a 100GB file that would be a bad idea. But the best answer is: Guard the allocation you're actually worried about, don't ladle this into the fast path everybody has to deal with on every allocation.


People say that "well if allocations fail all bets are off" but can't you pre-allocate memory for error handling?

Like sit down, figure out all the things you'll want to do on an allocation failure, and once you have determined that you slice a little chunk of memory when you start your app (and maybe _that_ fails and you can't do anything). and when you hit a failure you do your think, then tear stuff down.


It's what we used to do in the days when 4MB was a lot of memory. Batch programs would just abort but interactive programs had to have enough reserve to fail gracefully, possibly unwinding and releasing things until they could operate better.

Now that I see interactive programs taking a gigabyte and the system being ok, I guess we're in a different regime.


What if the failed allocation came from a different allocator/heap than from where the allocations for string logging came from?

In general Don't assume anything about your global process state just because one allocator fails.


Mhm, thanks.

It never occurred to me (being in non-embedded land) that returning an enum as the error or a &'static str instead of a heap structure like String, could also fail.

Seeing that Result isn't part of core, but of std, this makes sense.

Just to tickle my nerve though: theoretically speaking, with your example, it would work, right?

I couldn't allocate 100GB (because OOM or not even enough RAM to begin with) but it could be that the system can allocate the needed memory for error message just fine.

Very interesting.


Result is part of core [0]. Result data and/or errors can be stack-only data. The parent was just saying that many people that say they want to guard against out-of-memory issues aren't cognizant of just how difficult that is.

Add to that that several operating systems will lie about whether you're out of memory, so the 'error' or failure will often not be on the Result() value but come in a SIGKILL instead, it's just adding complexity.

People that are actually worried about it and no how to deal with it, will be coding with a different style and can use the alloc library where/when they need to. (at least when it gets stabilized in Rust)

[0] https://doc.rust-lang.org/core/result/


Thanks for correcting my error.

I've never checked core before, so I did when checking up for this discussion.

I somehow missed Result. Silly me didn't search on that page, but ofc I found it on std

https://doc.rust-lang.org/std/result/index.html

Also thanks for clarifying that values of Result can be stack-only!


Tialaramex answered this in their post already, and you almost answered the question yourself:

> I could just .unwrap() or .expect('my error message') it.

Panicking can allocate. Allocating can fail. Failing can panic. Panicking can allocate. Allocating can fail. You can bite yourself in the ass like a real Ourobouros.

IMO, a prerequisite to using fallible allocation APIs should be attempting to write your own allocator, handling the weird and wacky problem of initialising a data structure (for the heap) in such a way that if it fails, it fails without allocating but leaves some hint as to what went wrong.


Oh, wow, I was under the impression that the error message would be stack only, no heap involved, but as Result is part of the std library and not of core, this totally makes sense.

So for `Rust for Linux` they also need to implement a `Result-like` type that is stack only based to solve this issue, right?

If so, cool, thanks, you just made my day by tickling my learning nerves! :)


It has nothing to do with Result, whatsoever. Result does not allocate. If you used a Result that way, you could certainly try to "gracefully" handle the allocation failure, but if you think it would be easy, you would be wrong. As Tialaramex said, you are probably just going to make the problem worse because it is very difficult to ensure you do not attempt to allocate during allocation-failure-recovery. Rustc doesn't and can't really check this for you.

It actually has to do with `panic!(...)`. When you use `unwrap()`/`expect("...")`, you use the panic macro under the hood; parts of the panicking infrastructure use a boxed trait object which could contain a static string or formatted String or anything else really. The box can allocate if it is not a ZST. I believe the alloc crate's default handler tries to avoid this kind of thing, so that it can't fail to allocate AGAIN in the failure-handling routine. It will likely do a better job than you could.

This is a live issue at the moment, so to go into any more detail I'd have to read a bunch of recent Rust issues/PRs.


An addendum to tie this back to the original discussion: the reason kernel devs want these APIs more than userland is that (a) in a kernel, panicking = crashing the computer, which would be bad, and (b) they have a much bigger toolbox for handling OOM.

They can kill entire misbehaving processes. What are you going to do in your little program, clear a cache whose objects are sprinkled evenly across 150 different pages? You would need more control than you get from blindly using malloc/free/rust_alloc globally. Something like memcached would be able to use these APIs, because it uses its own allocator, and knows enough about its layout to predictably free entire pages at once.


> panicking = crashing the computer

That isn't very accurate. In Rust when programming in no_std, you can (must?) define your own panic handler:

https://doc.rust-lang.org/nomicon/panic-handler.html

Which you would define in the kernel. While I'm not going to speculate on exactly what the implementation would look like, you definitely do not need to "crash" the computer. I haven't done any kernel programming, but I'm guessing the kernel could do some things at that point with shared memory space that is already allocated to deal with this situation and try to recover in some way.

Edit: for example, I just found this in the kerla project: https://github.com/nuta/kerla/blob/88fd40823852a63bd639e602b...

That halts now, but it probably doesn't need to, or could do it conditionally based on the contents of PanicInfo.


Mm no, it's pretty accurate. For a start, notice that the Linux community has been very clear that panicking is unacceptable. The reason is that they cannot realistically do anything to recover.

> panic handler [...] Which you would define in the kernel. While I'm not going to speculate on exactly what the implementation would look like, you definitely do not need to "crash" the computer.

The panic handler loses so much of the context that crashing the computer is the only thing you can practically achieve. You can't retry an operation generically from with a panic handler, it doesn't know anything about the operation you were attempting. The OOM handler gets a Layout struct only. You could try unwinding or something, but within a syscall handler, I don't see how anything good can come from that. Unwinding in the kernel is simply a terrible idea. What else are you going to do?


I disagree that PanicInfo loses so much context. PanicInfo caries an arbitrary payload of &(dyn Any + Send).

Now there is a lot that the allocator could do. If you wanted something to be retriable, it could be interesting if the thing that failed was an async task. If so, that panic info could carry enough information to say, the failure was an OOM, here’s the task that failed, and it is marked as retriable. Yes, this would require a store of tasks somewhere in the kernel. Then based on it being an OOM, see if any memory can be reallocated before retrying, or wait to retry until it is.

This is where theoretically a new async based Rust kernel, especially a micro-kernel, could be interesting. Is stack unwinding in the kernel a bad idea? Maybe. Can it be done in Linux? Maybe not, maybe it’s too much work to track all this information, but I disagree with the conviction with which you right it off.


Thanks for the thorough explanation!


Result already is “stack” based, or sized. It also already exists in core: https://doc.rust-lang.org/core/result/index.html

The Error type currently isn’t in core, but for other reasons, that just got resolved: https://twitter.com/bascule/status/1452029363197784071?s=21


AFAIK one other thing to note is that in Linux userspace, malloc (or fork) might succeed, but accessing the memory later can fail because of memory overcommit.


Just in case you don't know about it: https://redox-os.org/ could be interesting to you :)


Yes, it was/is for only the duration of the summer (school) break.

Has some very weird constraints regarding borders when combining with the eurorail/interrail though.



Ah, the good ol' KISS - complexity kills.


But not Down Under scnr


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: