 Alright, so my name is Sonny Scroggen, Scrogg's son on the internet, and this is taking Elixir to the metal. So apparently one of my co-workers when he heard this talk title thought that it was going to be about Elixir and like music, like metal, and it actually was, but Johnny forgot to bring the drum set back here. So it's actually going to be a different talk. So this talk is about Elixir, right? And as we all know and love, Elixir is a dynamic functional programming language, it's built for scalability and all that good stuff that we've heard about already in this conference. It's known for this like trinity of features like concurrency, fault tolerance, and distribution. And it's built on top of this rock solid, awesome thing. But one thing that I think a lot of people don't really understand when they first start learning Erlang and Elixir is that this thing is like the ultimate orchestration platform. It allows you to build systems that you don't need to build everything in Erlang or Elixir and have it available inside your code. So it can talk to the outside world and you can also build supervision trees that actually support, you know, spawning processes like external systems. Maybe you have like a Python script that you need to run and because that code is already stable and doing what it needs to do, but you can write it in Erlang so that it actually starts that process and has a port communication. And if that process dies, then you can actually supervise it and have it restart and do whatever. So actually Erlang was originally built for Telcom and believe it or not, like you have to deal with hardware and stuff in Telcom. And so all of these features are built into this runtime system that we know and love. And so there's various ways that you can do this. And so you can use ports. There's this thing called Erl interface, which is really interesting. And there's also a thing called J interface, like if you want to do Java, which I don't know why you would. There's this thing called C nodes, which is really, really cool. So you can actually build external systems that use the Erlang distribution like protocol to act like an Erlang node. And you can send messages to it and it can send messages back, et cetera, but it's like an external system. There's this thing called port drivers as well, which is, there's a lot of the internals and stuff of various things are built with port drivers. And then there's this thing called native implemented functions or NIFs. And some integration examples, there's, MIX uses ports to like when you have like Git dependencies, for instance, it's going to use a port to actually like reach out to the Git binary on your system. Erlang's iNet driver is a port driver for all the networking stuff that happens within the beam. So like TCP, UDP, all that stuff. It's written in C, it's really, really fast. And then there's also this NIF called Jiffy. And it's a JSON encoding and decoding library. And so it's really, really fast. So if you're using like Poison or whatever, if you want to have like a really fast NIF or a really fast JSON encoder, then you can use Jiffy. And it's written in C and it's really fast. So NIFs, that's basically what this talk is going to be about. We have, it stands for native implemented functions. They're usually implemented in C or C++ sometimes. And of course they're, because of that, it's reserved for wizards, you know, these people that actually know what they're doing. Why the hell would you want to write a NIF in that case? So Erling's fast, but it's not, it's not that fast, right? It was not really designed for raw CPU throughput. It was designed for this fault-tolerant system and giving you a lot, it's a lot more about how to build fault-tolerant systems versus speed, right? And so you'll want to write a NIF if you have to talk directly to hardware, for instance, since you can't really do that directly in Erling. Maybe you need to interop with some graphics library or whatever, or maybe the functionality that you need already exists in some C code and you just want to pull that in because it would take much longer to rebuild everything in Erling or Elixir. So the one thing that, don't try to read all this because it's really tiny, but when you get into NIFs, you're going to read this warning when you actually look at the documentation. And it's like, basically, the first rule of NIFs is don't write NIFs because there's things that you have to actually know, otherwise you can cause catastrophic failure throughout your entire system, which is bad, especially since we're running on the beam because we have these fault-tolerant guarantees and things like that. NIFs themselves are called just like any other Erling and Elixir code. You have a module, call a function, and instead of calling the normal byte code that would be running inside of that module and that function, it's going to actually reach out and load some native code and run that. So NIFs live in dynamic libraries, so depending on your operating system, it's going to be like a shared object.so or a DLL if you're on Windows. NIFs end up, what happens is they actually replace the normal Erlang byte code with this native code when the module is being loaded into the runtime. So NIF functionality, essentially, it can read and write Erlang terms, things like binaries. There's this really cool thing called resource objects that I'll talk about. And then you can also do native threads and concurrency as well. This is kind of how you would set up a NIF in Elixir. So if we were creating a JSON library, there's this module attribute called onLoad and it takes an atom of a function that it's going to call, so it takes zero Erty. So we have this init function, we're going to call that, and this we're just going to use the code priv to find the path to our actual dynamic library. And then we call Erlang loadNIF, and that will actually then do all of the replacement and stuff. And then so these functions here that we have, we just return errors. You can also provide the fallback actual implementation of these functions if you want. So if you want to pull in poison or whatever and just have it call it, if for some reason it can't actually load the native module. But otherwise, you just return an error if something bad happens. So here's the structure of init and C, EAC. All right, so Erlang provides a NIF API, and you can pull this in with the ErlNIF.h. And then you just basically have to implement all your functions, which they all have the same structure. So it takes an ErlNV. As the first argument, we have a count of how many arguments are being called, so basically the Erty of the function. And then we have all of the actual terms, the Erlang terms that are being passed into the function. And it's just a vector of Erlang terms. And then we just need to return an Erlang term. This ErlN thing, it's basically passed as the first argument. It represents an environment that hosts Erlang terms. And it also contains transient information about the calling process. So everything that happens in Erlang happens in a process. So every time you call a function, the caller of that function gets passed in as kind of a contextual information about the calling process, and it's available inside the NIF, or available inside the environment. All terms that basically belong to this environment, they're destructed when the environment goes away. And really it's an opaque type, so you can't really do anything with it other than just like pass it into the Erlang NIF APIs. Similarly, the Erlang NIF term, it's an opaque type. This kind of represents the Erlang term in C, and it belongs to this environment that's passed in as well. So the next thing we have to do is we have this ErlNIF func type. And what that does is it allows us to return a list of structs that represent the NIF on both the Erlang side and the C side. So we have the first thing in the struct is like a string representing the function that's in our Erlang module, or Elixir module. Then we have the Erity, and then we have like a pointer to the function that actually is going to do the work in C. And then we have the final thing we need to do, which is to kind of initialize everything. And this we tell it, hey, this is the module that you're going to attach yourself to. Here's the list of all the functions that we have, and then there's like these nulls are a bunch of other things that we don't care about, like how are you going to handle reloading and loading and various other things. But obviously I have no idea what I'm doing. I don't know C very well at all. I would never trust myself to write C, even if my life depended on it. That brings us to Rust. So Rust is a systems programming language that's built by Mozilla, and it's a really, really amazing awesome language. It's like a high-level, low-level language. So it's blazingly fast. A lot of function, you know, the people are writing, rewriting a lot of C programs that are notoriously like security prone, like for bugs and stuff, and trying to make things more secure, and because of the way that Rust works, it allows you to build programs that won't compile unless they are actually safe, and like basically just removes whole classes of these bugs that we see that cause all these security vulnerabilities and things. And not only that, but obviously like programs that crash at runtime. So Rust has a lot of guarantees for fault tolerance and safety and these things, which is really, really awesome. So some of these accolades that it boasts on its website. So zero cost abstractions. There's this really cool like trait-based generics that they have that allow you to kind of, it kind of gives you this object-oriented feel, but it's not object-oriented, which is really nice. It actually does have a lot of functional approaches. It's got like monadic types, so if you're like a type nerd and you like Haskell, it does have some of the stuff that Haskell has. But it also has like pattern matching. It also has efficient C bindings. So when I first started learning about Rust, I was really wondering like how it would be so cool if I could write niffs in Rust instead of C. And so I was thinking, well, if I'm thinking about this, surely someone else is also thinking about this. And so I really wanted to be able to write niffs in Rust and see how that goes. So the first library that I came across is called Erlang Niffsys, and it's essentially, it's basically like a generic binding of all of the C API, but translated into Rust. And so I started taking a look at this, and it was like, oh, sweet, I can totally like write niffs in Rust instead. The API here, this is like really just like a thin wrapper around the Erlang C API. So all the function names are like really like long and C-like, and it's very much almost like translating the C API, so you still have to like pass around pointers and do all these kinds of things. And so it doesn't feel like really like rusty. So I thought, well, maybe I'll just like build a higher level library on top of this. And I ended up coming across this library, which was like, yes, I don't have to write it. That's so awesome. But when I did find this library, it wasn't quite compiling on Mac, so I couldn't use it. So I got involved. I sent a couple pull requests, got it working, got all the Travis CI stuff working and involved in like within just like a couple of PRs, the maintainer Hans was like, well, since somebody else other than me is actually interested in this, why don't you just, I'll just make you a collaborator and we can like, you know, talk about this stuff. So basically it was like, all right, sweet. Now I'm going to actually like submit a talk about this stuff. And so I submitted a talk for this and I actually gave it about a year ago in NBC London. But I was really stressed because literally all the functionality that I wanted to talk about wasn't in this library yet. And so there was like a couple of months where it was just like working hard to like get all these things available so I could actually talk about them. So Rustler is basically, it's a library for writing niffs in Rust, obviously. It provides, you know, facilities for generating the boilerplate that you don't need and interacting with the Erlang VM. So it handles all the encoding and decoding of Erlang terms to Rust types and vice versa. And it also, it catches any panics that you might have in your Rust code. And it catches that before it unwinds back into C. And so really the reality with that is that using Rustler you should not be able to crash the beam with your niffs. Which is not, you can't guarantee those types of things with C niffs. So if you use it incorrectly you can completely destroy and crash the beam, which is totally sucks. So getting started is really simple. We actually have like an Elixir mix task to like generate stuff for you. So we can create a mix new JSON. We add Rustler to our dependencies. This is the current version, but the like changes that I'm showing are actually going to be in master. So if you didn't know about this in your depths you can actually like add this sparse thing that tells it like a directory that you want to use. So also you can use github instead of git and you can just use the username and the library or the repo. So fetch the depths and then we can use mix rustler.new. And this is going to allow us to like generate our project. So we tell the module name which is JSON and it's going to go ahead and generate a bunch of stuff for us. We can also like specify here that we want our library name like our nift to be called fast json. So then you can add the rustler compiler to the list of compilers in your project. And then we have this rustler crates configuration here which allows us to specify you know all the different nifts that we have in our project. So we tell it the path and then we can tell it like what mode we want rust to be compiled in. So depending on if it's if we're in production then we want to be like compiled in rust for production which make it faster. Otherwise we'll just use debug so that it's faster to compile. So this module that we wrote before with rustler pulled in it shrinks down to just this. So you just use rustler and then it goes ahead and generates the init function for you and all that kind of stuff. So this is what our librs file looks like. So we have a couple of extra crates that we pull in. These are like external packages outside of the crates system in rust. So we pull in rustler. We pull in this thing called lazy static which allows us to like have atoms that are statically created and all that kind of stuff. And then we have extra and create json so I'm like pulling in an existing json parsing library that's already fast in rust. And then we have like a couple of modules. So we have a decoder module and we have an atoms module. And the atoms module is just simply like our atoms that we're going to be using within inside of our nif so like you know okay and stuff like that. Then we have this rustler export nifs so we specify the string elixir.json. For those of you new to elixir when you compile your module it always is elixir. whatever your module name is. And then we give it a list of similar structures before where we have the function name inside the module the arity and then some function within our rust code that's going to handle that. Alright so let's take a look at the simple approach of just using existing json parsing library. So we're just going to basically take a string or a big binary and we're going to decode that into rust types and then we're going to convert those rust types into erlang terms and then return it. So this is the decoder. Just importing a bunch of stuff and we have our decode function which is going to take the environment as the first argument and then instead of having the count of arguments and like a list of terms we just have an args and it's basically a slice of terms. And then this returns a result which is either a term or an error. So in rust one of the things that's really, really cool is that you can't throw exceptions or any of that kind of stuff. You literally just have types that you return and if you don't return the right types then it won't compile. So we don't have to worry about throwing exceptions all over the place or whatever. We can just use results. So here we just fetch the first argument out of the terms here and so we just call decode on it and this question mark is really, really cool. It's basically like it's a short way of basically saying, hey, do this thing and if it returns a good thing, if it returns okay then just like give me the result, like unwrapped. But if it's an error then go ahead and bubble the error up and then it's like early return of the error. So we get that, get our data and then similar to erlang elixir like a case we have match. So we pass our data into this JSON parse and it returns either okay in the JSON data structure or it returns an error with this error. If we get okay then we basically call this function called JSON to term. We pass in like stuff, we pass in the environment so that we can use it in there and then we return, we basically, this is like returning the tuple of okay as an atom and the term itself. So we're returning basically this like okay tuple. In the case of an error we just return error and give it the error reason. All right, so this is the JSON to term. It's basically just a recursive function that just goes through and tries to encode all of the JSON types to erlang terms and it handles maps and arrays and all those different things. So it just does that in a recursive loop and then essentially that's what we return at the end. All right, so let's time our nif. So we read in some big, big old JSON file. This is like eight megabytes of JSON, which I guess is not really that big but whatever. So this is the way we can actually like time how long it takes to run this thing. And so we call timer.tc and pass in the module function errors and then we get the time that it takes back. So this is like 1.5 seconds. So I have this like yikes here. Most people will be like, whatever, it just takes 1.5 seconds to parse that much JSON. And in most languages that's really, it's fine, right? You just have to deal with it. There's a lot of other programming languages that have foreign function interfaces and they're dealing with native code. In Ruby there's lots of, like most of the JSON parsing libraries are implemented in C as well. If you get a big file and you have to parse it and it takes a long time, that's just the way it works, right? But in Erlang it's very different. And so I'm going to get into kind of the details of why this is and why this simple approach of using an existing library is really the naive approach. And so when you are first learning about nifs, there's a reason why there's that giant big error or like warning and because there's a lot of things that can go wrong and one of them is taking too much time. So the part of the warning here is that a native function doing lengthy work before returning degrades responsiveness of the VM and can cause miscellaneous strange behaviors. And then this next part feels like one of those drug commercials like, you know, it's like strange behaviors, such strange behaviors include but are not limited to extreme memory usage and bad load balancing between schedulers. So this is definitely a problem. And really it's only something that I think that I've only ever seen really be a problem in the Erlang ecosystem. So to understand this we're going to take a little quick deep dive into how the Erlang VM works for those of you who are not familiar with it. So you have a machine. That machine has a bunch of cores. So let's say we have eight cores. On top of that you've got your OS and your kernel threads. And then you start your application. And it starts an OS process that basically has the beam running. And then the beam when it starts up, it also starts up a bunch of its own threads that it uses for various reasons. It starts up eight scheduler threads, one per CPU core. It also has a bunch of other threads for like IO and stuff like that. But for the sake of this demonstration, this is the problem that we encounter. So each CPU core has its own scheduler. The job of the schedulers is to schedule processes. So each scheduler has a run queue. And it essentially is like a list of all the processes that it's handling. And the run queue has got all these Erlang processes in it. And it just goes for each one, pops the head off, and it runs the process. It runs the bytecode and executes for a given amount of time. So the way this works is that because it's running bytecode, the VM and the scheduler can actually understand the context of how calls are being made. And so if the thing is just doing like CPU intensive work, then it's actually going to only have a limit of what's called 2,000 reductions. And so a reduction is sort of equivalent to a function call, but it's not really documented to be exactly what it is. So I've heard that it can vary depending on the operation that some function calls cost more reductions than others. Also, if you use Elixir to wrap Erlang functions, just because you want an Elixir API on top, you actually end up causing yourself more reductions, by the way. So, yeah, think about that. The other thing is that because we're running bytecode, the VM actually understands what you're calling. So there are a couple of other cases. If your process is trying to read from the file system, the VM knows that the result is not going to get there right away. So it actually puts your process and schedules you out and puts you into another queue, like the waiting queue. And it will wait for that message to arrive and before it actually puts you back in the queue. And same thing for if you are waiting, receiving a message as well. All right, so the other thing that's really important about this is that there's this thing called thread progress. And you can read about it if you go to the Erlang OTP repo and you follow this path here, this Erts emulator, internal doc thread progress. It's a really interesting write-up about how the schedulers communicate and stuff and why it's really important not to block. So the scheduler threads themselves, this is kind of like just a nutshell of how this works, but scheduler threads, they share some data structures. And rather than protecting them with locks or ref counts, which tends to be like a cause bottlenecks, they essentially share progress with each other and essentially they use this knowledge to know what data structures can actually be released or garbage collected or whatever. And so I think before they basically, the beam is amazing because they continue to make progress on making this thing more and more performant and this is just one of the things that they've done that is pretty cool. So the key takeaway here is you don't want to block the schedulers. You block a scheduler and it prevents thread progress and it makes other schedulers wait. So a block scheduler can't run processes, which is a big problem. And so a NIF should never take over the scheduler for more than one millisecond is what the docs say, but obviously you want to return as fast as possible. And the problem is that reductions are actually not counted for NIFs. So when you call a NIF, the scheduler has no idea how long it actually is running for. And the main problem is that if a scheduler is doing this type of thing for a very long time or very frequently, the beam will actually start shutting down schedulers because the beam is built for efficiency and even power efficiency as well. So if it doesn't need all those schedulers, then it will actually shut them down so you're only running processes on two schedulers or whatever. So that can be a really big problem and in fact the folks at React, the database, they ran into a lot of problems at scale because they were running a lot of NIFs and also some other things as well. So actually some customers were seeing what they call scheduler collapse and took them a while to figure out this bug. So we need to count reductions. So we need a way to do that in our function. And so this is how you do it. You can get process.info, you can ask how many reductions you have and we can then call the function, we can do some timing differences and count the reductions as well and then send it back to the process. Interesting stuff. So when we count reductions here, we can see that running this, it only counted 10 reductions and this is way lower than it should have been. All right, so how can we do this better? One option is chunking. Chunking is actually kind of hard for most problems, especially like JSON parsing. You have to do it very iteratively and then you also have to be able to preempt yourself and return back at any given moment. So it takes a lot, it's a lot more work, which required me to actually build my own hand-rolled JSON parser in order to do this. So the way that you do this is you now have a, we have a function called decode. We get the data and then we call a function called decode init and decode init is our nif and we pass the result of that to this decode result and depending on what the nif returns, did I actually do, yeah, okay. So depending on the return values, we can chunk and continually call the nif over and over again. And we actually have two nifs now, we have decode init, which initializes the thing and then we have decode iter, which is like iterative, like an iterator. And that's the one we actually call over and over and over again. So if our nif returns more something called a resource and then an accumulator, then we'll just essentially call this decode iter recursively over and over again. And then we'll also call decode result as well. So this brings us to this thing called resources. So there's a thing called resource objects and essentially it's like a smart pointer and it's like a safe pointer into some data structure that's inside the beam that you can't really use it, it's opaque. And if you actually like IO inspect the thing, it's just an empty binary. So you can't use it, but it essentially is this thing that you can pass around back and forth between erlang and your nif. And it keeps track of all these various things, like terms and all that kind of stuff. Another thing that's really interesting about this is that you can actually pass it to multiple processes so you could essentially build a global mutable state thing that multiple processes can actually use at the same time, which gets us into a couple of things that's interesting. But these resource objects, they're not deallocated until there are no processes that are holding on to this value. So once the last process is no longer using it, it gets garbage collected and then it's removed. So this is now our updated librs and we basically now have this parser and we also have a sync. The sync is where we just throw all these already encoded terms. And let's see. So we now have decoded knit, takes two arguments and then we have decode iter, which is the same type of thing. We also have this last thing is what we want to do on load. So on the previous one, we had none there because we didn't have to really do anything extra. But on this one, we actually have to do some work. So when the nif loads for the first time, it's going to call this load function and this takes an environment and allows us to initialize this resource. So we say resource struct in it and we pass in our type, which is our parser resource here and then we're good to go. And then we just return true. So here in our decode init, we have the same structure as before. We get the environment and all that kind of stuff and we also have our parser resource and it actually has a mutex of our parser. And the reason why we have to have the mutex is the same reason why I said you can actually pass resources around in multiple processes and so Rustler protects you against that by making you actually wrap it in a mutex because if you did have two processes that were trying to access the resource at the same time, if they were scheduled on two different schedulers at the same time and they were both trying to access the resource at the same time, this gives you a way to lock it and make sure that doesn't happen. And this won't compile at all unless you use a mutex, which is really cool. So similar to before, we grab the source, the data, we get our resource by creating a new one and we have this resource arc, which is atomic reference counter. We pass in our parser resource with a mutex with our parser that has the source and that of course is gonna go through and eventually it's gonna parse stuff. But next we create a vector of terms and it's just an empty vector right now and then we call our decode iter nif. So we call our decode iter nif and we actually pass in the environment and then we pass in our vector of the resource and the vector and then our empty vector and these are pushed in as terms and so this is so that we can like initially get called and then we immediately call ourselves so that we can chunk it. So decode iter, we actually pull the resource out, we create a vector of terms or we get our vector of terms, we call it the sync stack and then we create the term sync and we actually just like try to parse. So then we do resource.zero.try lock, this is gonna like get a lock on our mutex and depending on what that returns, if it returns an error that means that somebody else already has the lock so that's probably bad so we have to like bail out and we just return an error and otherwise we get this mutex guard and we just return the guard and we have this while loop here so we have while not consume time slice and then we pass in like a one here which I still need to figure out the best way to actually call this function appropriately. You're supposed to be able to like do some math and calculate approximate your like the amount of time you're gonna take or whatever but this is kind of a way to like actually accumulate accumulate reductions in the NIF itself so it's kind of a way to just tell the VM hey I'm actually doing work and tell me whether or not I need to like bail out so this thing will return true or false and so while it's false we will basically just parse some stuff so like we'll just keep calling it over and over again and then when we're done so if we're not done if this thing returns true or whatever then we have to like return this more tuple here so we have more and then our resource that we're passing back to Elixir and then our current like stack so it's like an accumulator that we can like pass back in and just keep pushing terms back on so now let's take a look at this we can see now that we've run that and we're now showing that we have like 11,000 reductions now which is much much better alright so that was cool there's also another way that you can do this in Erlang and it's called like basically rescheduling so you can do some work in your NIF and then like actually reschedule it in the NIF instead of returning back to Erlang or returning back to your Erlang code the problem is right now is that the way that the E-NIF schedule NIF API works is that we currently don't have a way in Rustler to guarantee that you're going to use it correctly so we've decided not to even put it in Rustler at this time so because we can't guarantee at compile time that it's going to be used correctly we're waiting until we can actually figure out the best way to do that so but when you combine like the idea of this rescheduling yourself along with consumed time slice it allows the chunking to actually be done directly in the NIF and the NIF itself will be rescheduled rather than like rescheduling it in your process you know space or whatever so yeah so it's kind of cool the other thing is threaded NIFs and this is actually I think a really cool way of doing NIFs specifically that you call a NIF and then that NIF basically just spawns a thread an operating system thread to go and do the work and then when it's done it can actually send a message to your process with the result and this way you end up getting when your first initial call to the NIF can return like immediately it can return like okay or whatever and then you just sit there in a receive block and wait like this so we have decode threaded now and similarly we just get the string out we use rustler thread we say thread spawn and we get a caller environment here and then we also get our own environment that we get here and then when we parse our JSON and we can basically do all this work in the thread so now we've basically pulled back out and pulled this JSON parsing crate from Rust back into our library because it's much faster and a better library than me writing a hand-written parser so once it's done we return like the okay and the result of it or an error but immediately down at the bottom here we're actually returning just okay like okay cool we got it and then right here this is kind of like how you would call it in Elixir so you would call decode threaded which is the NIF it's just going to return okay immediately and then we just go into a receive block waiting for the result so pretty nifty and then we can also like do some timeout stuff as well if we want to alright so the last thing I want to talk about is dirty NIFs so in OTP 20 they made this like on by default for a while it was experimental I think in like OTP 18 or something like that and so the idea is that now not only do you get like if you have eight cores you get eight scheduler threads now you also get eight dirty scheduler threads and the cool thing is that these scheduler threads are specifically for like doing lengthy work that you can't guarantee is going to not degrade the system so they don't operate on the normal thread or normal the normal scheduler threads there's some interesting things too that you can actually schedule your you can schedule your NIF on the dirty scheduler and then you can also have it scheduled on the regular scheduler and you can kind of flip flop and go back and forth or whatever which is kind of interesting so the way this works is you pull in scheduler flags here and when you're exporting your NIFs you just add an extra argument here to say it's dirty CPU there's also dirty IO as well and so you can specify that you want your NIF to run on these other schedulers instead of the normal one and obviously even in the case of doing dirty NIFs you still actually do really want to try and return as quickly as possible because if you end up hogging up a bunch of CPU time or whatever on the dirty schedulers it's also still going to eventually start affecting the system so the rule of thumb is that you still always want to have well-behaved NIFs and write them appropriately so that they don't take up too much time on either type of scheduler so with that I'm finished but this project has been really interesting to me I don't have a systems programming background or anything like that so it's really gotten me a lot of ideas that I want to explore and try to get out there and also encourage other people to explore and experiment with things so some of the ideas that I'm really interested in doing is to actually take a lot of the awesome things that are happening in the Rust community and maybe an HTTP server for instance and actually build that so that it is kind of done as a NIF and be much faster and more efficient but obviously there's a lot of stuff that needs to be learned along the way to do that but yeah, Rust is awesome Elixir is awesome and I think they work really well together so with that, thank you