A Rusting Rubyist IV

Rust Structs and Arrays in Ruby


This is part four of a series where I try to stumble my way through creating a Rust web-scraping library that will be embeddable in a Ruby module. If you are interested in starting from the beginning you can check out all my posts here: https://medium.com/@mfpiccolo

Follow along with this blog post with the part-4 branch of the scrape repo.

Last time in ARRIII, we set up a lib that allowed for Rust HTTP requests to be embedded into a Ruby module. So if you are following along that means we can pass and manipulate numbers and strings back and fourth between Rust and Ruby and make HTTP requests. That is looking good but we are going to need a bit more advanced structures than just strings and numbers to make this module useful.

Today we will learn how to work with Structs and Arrays.

Hello Struct!

To get right into things lets create a Rust function that using structs. Remember to use `#[no_mangle]` and `pub extern` for the function declaration so you will be able to use the function from FFI.

Here we are setting up a struct called TwoNumbers which has two fields representing i32 integers. The function accepts a struct of this type and adds the two numbers together. Lets try our test out and see if it works.

$ cargo test
running 1 test
test it_works ... ok

Nice. Now we can jump right into Ruby to figure out how to pass a struct in from a Ruby module.

Ruby Time

Lets break this down. We are using the FFI gem to create a module Scrape. On line five, we are importing the rust lib which we created above. As you can see the TwoNumbers looks just like the struct we built in rust. We then attach the Rust function on line 12, which accepts the FFI TwoNumbers struct and returns an integer. At the bottom of the file we have a goofy unit test to tell us if the output is correct.

$ cargo build
$ ruby scrape.rb
Gone Dun It!

That is good stuff. We now know how to pass in a struct from Ruby and work with it in Rust. This will come in handy. Lets now try to return a struct back to Ruby from a function.

Ruby Struct -> Rust Struct -> Ruby Struct

So we still are using the same TwoNumbers struct but this time we are going to implement a function. This is basically like writing and instance method in Ruby. Now we will be able to call the `plus_one_two_each` function on an instance of the TwoNumbers struct and it will add one to the integers for the `first` and `second` keys. One thing to note here is that mutability is inherited which allows us, on line 8, to set the instance of TwoNumbers to a mutable variable and then mutate the fields of the struct.

Now back on the Ruby side we just need to attach our function to the FFI module and set up our tests to check that the foreign function interface is working as expected.

$ cargo build
$ ruby scrape.rb
Wooooo!
Wooooo!
Wooooo!

Make way for Array

Now we are getting somewhere. Strings, Integers and simple structs are great, but we really need to handle collections and what better way to do that in Ruby than Arrays?

To get started we are going to need to figure out how we want to work with list/collection data on the Rust side. From reading the docs, it seems that the best type to use is a Vec. From the docs:

A growable list type, written Vec<T> but pronounced ‘vector.’

To show this functionality, lets build a function that will build an array of the standard ASCII character set. To start, we will just get the iteration and conversion of types down.

Fairly simple function here where we are just iterating over a range and converting the integer into a char. (33..126 happen to be the standard ASCII set). Time to make use of the Vec.

Here, on line 3, we are using the vec![] macro to set up a vector that will become a list of strings. Then on line 7 we are converting and then pushing each character into the list. Finally this method returns the list.

This is all good but Ruby doesn’t know what a vec is so we are going to have to do some conversion to get it over the boundary.

For this we will need a new Struct. One that knows what FFI needs to build an array.

We have declared a struct that can cross the boundary with the repr(C) flag. The function that we are implementing will convert a vec to a RubyArray struct and set the values of the struct to some libc types. Basically what we need here is a pointer to the start of the data and the length of the list. We will see later on how FFI will use this info.

Now we can make use of the RubyArray struct.

All we did here was convert the string on line 30 to a CString and then a pointer before pushing it into the vec. Then we can use our from_vec function to create a RubyArray struct that we can pass to Ruby. Lets get to FFI so we can use this puppy.

Here is our FFI code. On line 7..14 we are setting up the struct that will handle the info we send over from rust and the conversation method. We attach the function and Boom. You got yourself a Ruby Array.

$ cargo build
$ ruby scrape.rb
["!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}"]

ASCII for days! More importantly Array for days!

Next Time

We are really getting somewhere. Returning these more complex structures is quite a big step towards our goal of creating an embeddable web scraper. It is nice to get that bit out of the way because we have something exciting coming up and something that Rust is particularly well suited to handle. Parallel/Concurrent programming (apparently these are not the same thing). We will be learning how to make and record HTTP requests in parallel!

Special Thanks

The rust community, for the most part, is pretty nice to newbs so don’t be afraid to ask a Stack Overflow question or get on the rust IRC channel. Special thanks to Stack Overflow users Adrian, shepmaster, Chris Morgan, Vladimir Matveev and DK. Also Steve Klabnik for doing a great job on the docs.

And of course don’t hesitate to hit me up in the comments or on twitter@mfpiccolo