Elixirizing our Rust
(or) Elixirifying? Derustification?
If you recall from the last post, we succeeded in our goal of generating some art. Or something resembling art, at least. A stepping stone on the way to art, let’s say. We’re not going to get any more artful in this post, but instead we’ll be taking time to clean up what we’ve accomplished so far to provide a solid base to build a full library off of.
This post is part of a series:
- Part 1 - Generating Art in Elixir
- Part 2 - Actually Generating Art in Elixir
- Part 3 - Elixirizing our Rust (this post)
- Part 4 - Let's Draw a Line!
So, where did we leave off?
The library as it stands
Right now we’ve got the Xairo.Native
module that provides the function signatures we can map to our Rust NIFs:
defmodule Xairo.Native do
use Rustler, otp_app: :xairo, crate: "xairo"
def new(_, _), do: :erlang.nif_error(:nif_not_loaded)
def paint(_, _, _, _), do: :erlang.nif_error(:nif_not_loaded)
def save(_, _), do: :erlang.nif_error(:nif_not_loaded)
end
Except for new/2
, each of these functions takes as its first argument an Elixir Reference
that maps to an in-memory Rust struct that holds the cairo ImageSurface
and Context
objects:
pub struct CairoWrapper {
pub context: Context,
pub surface: ImageSurface
}
Functions in Rust accept the reference and return it, allowing us, as we saw in the previous post, to chain functions together with the |>
operator in a more Elixir-y way, but the fact remains that we’re passing around a Reference
struct which is, to put it mildly, useless, when it comes to understanding what that reference represents.
So let’s create a struct that gives us some basic information about the image, along with storing the image’s reference
Xairo.Image
to the rescue
defmodule Xairo.Image do
defstruct [:width, :height, :reference]
def new(width, height) do
reference = Xairo.Native.new(width, height)
%__MODULE__{
width: width,
height: height,
reference: reference
}
end
end
Ok, great, we’ve a struct that tells us, for now, about the width and height of our image, and provides the reference to the in-memory image so that we can pass that back to Rust. But, our NIFs expect a Reference
to be given, and now we’ve got this Xairo.Image
struct. But, the Xairo.Image
struct has a reference
field, so we can pass that to our Xairo.Native
functions.1
To do this, we’ll need functions that take a Xairo.Image
and arguments, extract the reference
from the image, pass it and the other arguments to Xairo.Native
, and return the full Xairo.Image
struct so that we can continue using the |>
operator. For a public-facing API, the root module Xairo
seems like a good place for these functions to live.
defmodule Xairo do
def paint(%Xairo.Image{reference: reference} = image, red, green, blue) do
Xairo.Native.paint(reference, red, green, blue)
image
end
end
For the sake of brevity we’ll exclude the other functions, but this example should be enough to show the path we’re starting down.
If we hop over into an IEx console, we can see that our previous example
iex(1)> Xairo.Native.new(100, 100) \
...(1)> |> Xairo.Native.paint(0.5, 0.0, 1.0) \
...(1)> |> Xairo.Native.save("test.png")
#Reference<0.3888755325.2009989122.47088>
can now be written
iex(1)> Xairo.Image.new(100, 100) \
...(1)> |> Xairo.paint(0.5, 0.0, 1.0) \
...(1)> |> Xairo.save("test.png")
%Xairo.Image{ ... }
It’s not much to look at, but under the hood we’ve built the following framework with clear demarcations that gives us a base to expand on:
Xairo
/ Xairo.Image
<–> Xairo.Native
<–> Rust
The user calls functions from the Xairo
or Xairo.Image
modules, which are delegated to the Xairo.Native
module, which provides the bridge to the Rust code. From there, the reference is passed back up the chain and finally returned as part of a Xairo.Image
struct.
In the next posts we’ll start looking at expanding our function palette to start drawing shapes on the image, as well as how to avoid some of the code duplication that’s going to start showing up in the Xairo
API functions.
Thanks for reading!
Footnotes
-
In theory we could create a Rust struct that maps to
Xairo.Image
and pass the struct itself back and forth, but this quickly becomes unwieldy when trying to sort out how to ensure that the nativecairo-rs
structs can be encoded/decoded safely. It’s not impossible, but it is the decidedly harder of the two options. We’ll see this put into practice a bit later on with some much simpler structs. ↩