This, IMO, is quite possibly the best instance of "best tool for the job" I've ever seen. Rust is great for writing high-performance code, and Erlang is fantastic for distributed systems.
I've been very excited at Rust's prospects as an alternative to C for extending all sorts of higher-level languages, but I'm most excited at the prospect of combining Rust with Erlang given the latter's focus on reliability. I'd love to figure out where Erlang programmers hang out to ask them what they think about using Rust for NIFs.
As an Erlanger, and maker of a NIF -> Rust library I wasn't allowed to open source (sadly), I can tell you I think this is excellent. My current science project is trying to rebuild some small components of BEAM and the Erlang Runtime System in Rust that are currently in C.
Mostly the above is an exercise in understanding the internals of BEAM better, because my ideal end state is to figure out what it would take to make an Erlang/OTP-like library for Rust (with task/thread/process/whatever preemption)... which is actually what originally drew me to Rust 3-4 years ago back when it was shaped more like a systems-Erlang (task supervision hierarchies, etc) and less like a renovated-safe-C/C++.
>Rust 3-4 years ago back when it was shaped more like a systems-Erlang (task supervision hierarchies, etc) and less like a renovated-safe-C/C++
Wow, any chance Erlang-like supervision and actor model in general might make a comeback in Rust? I'm sure the Erlang guys has thought about this, but no static type checking makes me a bit nervous. Erlang can be surprisingly strict (a good thing) in some ways, but as far as I can tell, you only discover any failures at runtime.
And depending on a whole slew of things that may or may not make this useful, or more likely tractable for your codebase, you can enable this to track down some shapes of race conditions:
-Wrace_conditions
The part where there's still a hole (now that most of the hole around Maps has been plugged) is in the message passing semantics. You can be conventional about how you write an API around the message passing to alleviate this, but there's still nothing stopping any random process from sending any shape of data to any other random process and thus essentially breaks Dialyzer's ability to enable static type checking through all paths. But as long as you have a catch-all matching clause implemented that dumps anything that doesn't explicitly match one of your types you're mostly fine.
One could probably discover failures at compile-time with Erlang/Elixir as well, especially if one is diligent with typespecs and/or pattern matching / destructuring.
For example, if I have the following Elixir function definitions:
def foo({:bar, bell}), do: IO.puts("got a barbell: #{bell}")
def foo({:baz, bell}), do: IO.puts("got a bazbell: #{bell}")
Subsequently calling `foo({:bat, "some value for bell"})` should be detectable by the compiler and thus generate at least a warning. We can luckily catch it relatively easily and painlessly at runtime with Erlang's normal insistence on process supervision trees, but it's still a crash that could be easily avoided without having to jump into the realm of full-blown "defensive" programming.
> As an Erlanger, and maker of a NIF -> Rust library I wasn't allowed to open source (sadly)
Presuming that this happened on company time, I'm curious to know if there's a company using Erlang+Rust in production or if this was just a fun side project. :)
You are correct on the first count. On the latter...
I've always been a fan of Twitter's mutual opt-in requirement for DM chatting. You're being followed. Which feels really weird and funny to write. But it'll be reasonably obvious.
I'm using Rust in exactly that capacity (along with ports). In fact, if it weren't for Rust, I'd probably steer clear from writing my own NIFs for the sake of my own sanity; coming from the Perl 5 world, where XS is king, I'm not exactly keen on splaying open Erlang's C API unless I have a language with which to do so without slicing off my own leg.
(Ports, on the other hand, are inherently safer due to being isolated from the Erlang VM, such that even if they crash they don't bring down the whole VM. They also communicate over STDIO, meaning that I'm free to use them to interface with components written in languages with less-than-pleasant methods of achieving C ABI compatibility, like Perl 5 and Ruby when not augmented with extra packages.)
On that note, Rust also has some wonderful implications for the Perl 5 world in terms of providing some semblance of safety around native Perl extensions. Same deal for Perl 6 (though its FFI mechanisms are infinitely nicer than Perl 5's).
Really nice to see this linked here, I was chatting with hansihe about getting the html5ever parser from servo into a nif and he magically created it here:
NIFs are the fastest method to call external code from Erlang/Elixir, as far as I understand. But I wonder how high the actual overhead is.
My understanding is that if I still want to ensure that the Beam VM can continue to schedule all processes efficiently, these NIFs shouldn't calculate forever, but return quickly to avoid blocking all other processes. So the straightforward way to ensure this for longer calculations would be to split the calculation in smaller, parallelizable jobs, if that is possible. But then the overhead of calling NIFs might actually matter, if you split them into chunks that are too small.
I've no idea how big the overhead actually is, I'd be interested in a rough estimate. Is it small enough that I can just ignore it entirely?
There's very little overhead in calling C (or C ABI shaped things) from Erlang. The reason why NIFs can trivially bring down the whole Erlang VM is because those libraries are linked straight into the same process space as the runtime itself, so in effect calling those NIF functions is roughly the same as calling internal VM functions that are written in C too.
There's some overhead in type-casting/data-type-interpretation, but not much. The internal representations of the core data-types in Erlang are mostly already represented in C in the runtime. There's some overhead in memory copies across that boundary too unless you make your data opaque to Erlang and always manipulate it via the NIF in which case you can get away with using NIF "resources" and more or less just pass a pointer and environment back and forth.
> My understanding is that if I still want to ensure that the Beam VM can continue to schedule all processes efficiently
It's worse than that, overlong NIFs can lead to scheduler collapse with your schedulers being put to sleep and stuck there.
> these NIFs shouldn't calculate forever, but return quickly to avoid blocking all other processes.
Recent BEAM (R17 and later) has the experimental feature of dirty schedulers which allows running long NIFs without risking scheduler collapse. There is one set of dirty schedulers for IO-bound NIFs and one for CPU-bound NIFs, they should be marked accordingly.
> I've no idea how big the overhead actually is, I'd be interested in a rough estimate. Is it small enough that I can just ignore it entirely?
It is lower than my usual solution which is to pipeline requests to another process over a pipe. That solution has an overhead of about 1us per call.
As for not blocking, the OTP team is working on getting dirty schedulers to be a first class system. The overhead there is also pretty low, but now you don't have to worry about blocking the schedulers anymore.
What is pricey in a NIF is conversion of data back and forth more than the overhead of the call itself. it is usually quite fast.
If you look at what NIFs exist up to this point and where Erlang itself tends to use that mechanism, they are all for very tiny uses, like type checks and such. But that's probably precisely because they're too scary to use for anything very large. The existence of a provably-safe and practical mechanism for writing them may cause and/or require some changes in the ecosystem to account for the fact it's now possible to write NIFs that may take a long time to run.
I wouldn't be surprised that at least one of this library or the Erlang VM itself develop an official way to easily use a NIF to run a longer-running process safely. There hasn't been a need up to this point because there hasn't been a such thing as a long-running NIF.
At the moment, if you're looking at something that may take several seconds you're probably still better off coordinating something over a port to an external process or something. Or working with this project to make it feasible to run a long-running native process.
Trying to write a NIF that could somehow yield back and then be "continued" later would be a royal pain. Cooperative scheduling was enough of a pain when we were in the hundreds of processes mostly doing nothing on OSes; trying to use it on an Erlang server is probably just infeasible.
I didn't just mean the mere ability to yield, which isn't that complicated; I meant code supporting the common use cases around having a long-running NIF, such as the person I replied to's comment about having some sort of build in support for being able to answer back to a PID or something. I could easily imagine a NIF library that provided an easy ability to set up an independent thread pool and work sharing mechanism specific to that NIF, which sends answers back to given PIDs, has timeout support or the ability to query it for progress, etc. All fairly straightforward stuff to expect to develop over time, but in a world where long running NIFs are too dangerous to hardly even contemplate, they haven't developed yet.
There's some some parameters in there that are going to be tricky to set up correctly (number of workers in the pool for a given NIF has a lot of implications at scale), but in the end it's not significantly different than communicating to a separate process on the some machine; it's all the same resources being used.
> to write a NIF that could somehow yield back and then be "continued" later would be a royal pain.
I know... nothing... about the NIF interface, but the way that I would want this to work wouldn't involve NIF resumption at all, but rather for NIFs to be able to put responses back on a channel.
Unfortunately neither quick internet searches nor searching the rustler docs revealed anything about a NIF/channel interface. I'm not sure how feasible it is to pass a channel into a nif and allow it to pass that into some thread and return, nor how good of an idea that would be.
This wouldn't make sense because NIFs are supposed to be just straight function calls that provide immediate returns. It's a blocking interface.
If you want the behavior you're talking about you would communicate via ports, or you'd write a NIF that maintained its own off scheduler thread(s), returned immediately, but kept a reference to the calling pid, and then sent a message to that pid at some later date. Several IO related NIFs do that today.
That's actually an excellent use for Rust in this case because it can help you make that multi-threaded implementation safer and more reliable.
That all makes sense, and the immediate return + threads solution you describe sounds like what I was imagining, I just didn't have the context to use the correct parts of the Erlang FFI. I'm glad that there is a pattern for that kind of thing!
If it's something that really can be split into small parallel jobs, then NIFs make sense here; the overhead is pretty low, since the NIFs are linked into the VM itself.
If it's something that can't be easily split into small parallel jobs, then you might want to go with a port, such that the long-running job doesn't hang your whole Erlang VM.
> This means you can write and run totally safe code in Rust, no worrying about segfaults.
This is not quite accurate; you can still segfault rust writing 100% safe code, for example if you have a large stack overflow ever since __morestack was killed.
Nah, just nitpicking. Though I guess the point is that AFAIK there's no documentation for all the ways that safe rust code is allowed to segfault; the distinction between a in-rust panic is that a segfault would probably crash beam, whereas an in-rust panic might be recoverable?
> there's no documentation for all the ways that safe rust code is allowed to segfault
I wouldn't say "allowed" to segfault. :P The behavior you're referring to is currently due to a deficiency in LLVM for non-Windows platforms. Here's the bug on the Rust repo tracking it: https://github.com/rust-lang/rust/issues/16012 Its resolution is long-awaited, to say the least, but it requires someone proficient with LLVM to do the legwork...
> an in-rust panic might be recoverable?
Anytime you're writing an interface where code is going to be calling into Rust via C FFI, you ought to be using https://doc.rust-lang.org/std/panic/fn.catch_unwind.html , which is specifically intended to prevent panics from crossing FFI boundaries.
Anytime our guard pages detect a stack overflow, an abort is issued with no chance to unwind. (The segfault bug mentioned above is caused by crafting data on the stack such that you bypass the guard page, which would go from a segfault into an abort in the presence of stack probes.) I did not intend to imply that there was any chance that a stack overflow wouldn't bring down your process, only to clarify Rust's stance on the theoretical memory safety implications. :)