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:


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

  1. 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 native cairo-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.