Porting Ruby agent to Rust

Hello community,

I'm a Ruby dev learning Rust. We, at my company, have a Ruby Sinatra agent for managing servers, it setup users, dbs, web pages, emails, config templates, lot of admin tasks.
It is written in Ruby and we will like to write it in Rust, the main reason is the memory consumption and the possibility to drop it as a single binary without any/many deps.

The Ruby agent is big and full of features, It is hard but we went really far in implementing the functionalities in Rust (using Rouille, r2d2), but its still hard, database interaction is tough (compared with Ruby).

We will continue anyway but I'm thinking in other options. I would like to split the http/rounting api (rust) from the scripts to reuse existing ruby code and migrate gradually different parts.
I know I can run commands (ruby scripts) from Rust and capture output (this will need ruby/rubygems installed) but what other posibilities do I have?
Could I embedd ruby in a rust binary, and package rust app + ruby code?
Can I have Rutie to load gems and execute code from there? If you could give me some example or projects to look into would be awesome.

At the end I think I have to migrate everything to Rust, but on the side I'm looking for different ways to aproach this migration.

Thanks in advance!

1 Like

After my post I started reading Rutie source code and found "VM::require(name: &str);", going the rabbit hole now. I still would like to hear ideas if you have.

In my mind, I would do it the other way around. Rather than have the rust code call the ruby code, I would personally try either:

  1. completely separate the two code bases and put a rust binary in front of the ruby code as a proxy of sorts if that makes any sense here to start off loading functionality that way

  2. make a rust module that is loadable from ruby. The way I’ve structured something like that in the past is to create a pure rust crate to handle some piece of the functionality initially. And then have a second crate which uses the first one but provides either an ffi interface or just makes a compiled loadable ruby module. (I did this with python and pyo3 not sure if there is an easy ruby equivalent). This has the benefit of keeping the new rust code completely clean and can be incrementally built up. You’ll continually be adding and removing things from the interface crate as more functionality gets moved into it, but it’s not that bad.

I’m not sure if either of these ideas would be new to you but I’ve attempted moves from ruby to python (This was a total mess) and python to rust and the second method is what I ended up doing for the python to rust case and it worked pretty well for me.

I think the first one is probably cleaner since you don’t need to interface between the two code bases. But I would guess it’s harder to make work since it’s very program dependent on whether functionality can be broken out that way.

Both of these methods though have the very useful property that all the intermediate states of it are runnable and can be deployed so you don’t need to worry that as you add features to the original codebase they need to be added to the new one which might have a different architecture. Obviously this means you would keep all the ruby deployment annoyances until the very end when the rust code has completely taken over functionality but you would get improved performance throughout the process.

It goes without saying that this is based on the codebase I’ve moved over which might have completely different properties to yours so this may not work at all for you.

1 Like

I was thinking the same thing.

I know nothing of Ruby or Sinatra but it seemed like a good idea if one wants to 'oxidize' any huge code base. And it is what has been going on this last year with our not so huge code base.

Basically if you can start from the bottom up, some leaf module(s) or such that you can rewrite in Rust then you always have the known working pyramid of Ruby or whatever it is to plug it into and check that it still works as expected.

By way of example, it would be crazy to say that one is going to rewrite Linux in Rust from scratch. But one might imagine writing device driver modules in Rust. Then one day higher level subsystems. Until eventually one gets to the top and all of Linux is Rust!

I hear people are working on that.

Of course the resulting program has the same architecture as the original and one could ask if all that work was was worth the effort for no noticeable gain. It might have been better to start with a better architecture from scratch. Like the Redox guys perhaps.

In short, I might ask, is it really necessary to recreate Sinatra in Rust?

1 Like

Thanks for the reply,

I'll give a better look to the first option, I had the idea to move http/api to rust, and run ruby scripts from command line and then migrate gradually. I'll have to test how that perform, for sure memory consumption will be reduced.
Also I want to have the Rust part running, then it will be alive and mantained, other way it will "rust" in the corner and forgotten.

Thanks

1 Like

No, I don't want to recreate Sinatra, I'm using Rouille crate as HTTP server and router. Recently I read about Tide (http server) and I like how they define routes and allow to have a state context you can pass around, but is async and I have to adapt the current codebase, so for now I'll stick to Rouille.

1 Like

If it proves to be too difficult to set it up such that the rust binary calls out to ruby, you alternatively can run them both in parallel where rust receives any incoming requests, and forwards along any that it doesn't know how to handle. That might eliminate the need to restructure any ruby code initially since it will still operate the same way. This will not be as helpful if there is state stored in memory in ruby that's needed for a lot of the requests, but there you can just add some private api's on the ruby side that could view/manipulate state.

1 Like