Actually Generating Art in Elixir
(or) the Episode of Oprah Where Everyone Got a Yak
As promised in my previous post, I’m going to do my best to keep the yaks contained. So I’m going to set out a simple, concrete goal for this post:
from an IEx session, create a new image, paint its background purple, and save it as test.png
So let’s dive in!
This post is part of a series:
- Part 1 - Generating Art in Elixir
- Part 2 - Actually Generating Art in Elixir (this post)
- Part 3 - Elixirizing our Rust
- Part 4 - Let's Draw a Line!
Create a new image in Elixir
To accomplish this goal, our API will need three functions
defmodule Image do
defstruct [...]
@spec new(number, number) :: Image.t
def new(width, height)
@spec paint(Image.t, number, number, number) :: Image.t
def paint(image, red, green, blue)
@spec save(Image.t, String.t) :: Image.t
def save(image, filename)
end
In Elixir common practice, the image we are working with is the first argument to every function, and the returned value from each, allowing us to pipe it through the function chain.
At the end of this post, we should be able to run the following in IEx successfully
Image.new(100, 100) |> Image.paint(0.5, 0.0, 1.0) |> Image.save("test.png")
Ok, so we know what we want the Elixir side of this to look like. But almost all of the work do accomplish these things is going to happen in our Rust bindings, so let’s move down the stack one layer and see what’s up.
Create a new image in Rust
Having followed the Rustler setup from the previous post, we have a mix application with a linked Rust library. For the sake of semi-clever naming, I’ve named this application Xairo
, and, after adding a few Elixir files, our application file structure looks like this:
.
├── lib
│ ├── xairo
│ │ ├── image.ex
│ │ └── native.ex
│ └── xairo.ex
├── mix.exs
└── native
└── xairo
├── Cargo.toml
├── README.md
└── src
└── lib.rs
The Rust xairo
library will compile and be loaded as NIFs into the Elixir Xairo.Native
module. Then we’ll use the Xairo.Image
module to create our user-facing Elixir API.
Reading through the cairo and cairo-rs docs, as well as some helpful blog posts that cover similar ground1, we discover that the two cairo objects we need to concern ourselves with primarily are the ImageSurface
, which is, well, the surface on which an image is drawn, and the Context
, which wraps the ImageSurface
and provides the API for drawing. Pulling from the previously linked blog post1, and splitting the code out into individual functions, we get something that almost matches our desired Elixir API
use cairo::{ImageSurface, Context, Format};
use std::fs::File;
#[rustler::nif]
fn new(width: i32, height: i32) -> Context {
let surface = ImageSurface::create(Format::ARgb32, width, height);
Context::new(&surface)
}
#[rustler::nif]
fn paint(context: Context, red: f64, green: f64, blue: f64) -> Context {
context.set_source_rgb(red, green, blue);
context.paint();
context
}
#[rustler::nif]
fn save(context: Context, filename: String) -> Context {
let mut file = File::create(filename);
// This won't actually work! because `surface` is not
// an accessible attribute on a `Context`, but we'll get to that...
context.surface.write_to_png(&mut file);
context
}
rustler::init!("Elixir.Xairo.Native", [new, paint, save]);
Now, for many reasons, this won’t actually compile. But, just for fun, let’s build it and see what errors we get. One of the great things about the Rust compiler is that its error messages will very often help point us in the right direction for how to resolve them.
Now, there are a lot of them, but a similar error pops up a number of times, so let’s start there:
error[E0277]: the trait bound `cairo::Context: Encoder` is not satisfied
--> src/lib.rs:17:1
|
17 | #[rustler::nif]
| ^^^^^^^^^^^^^^^ the trait `Encoder` is not implemented for `cairo::Context`
Remember in the last post we talked about this Encoder trait as the trick rustler
uses to let us pass data from Rust to Elixir and back. Unsurprisingly, the data types that ship with the cairo-rs library don’t implement this trait. But can we do it here?
No, we can’t. One of Rust’s safety tricks is that, in order to implement a trait for a data type, your library must be the origin of either the trait or the data type. We don’t own either, so we can’t do this. Is this the end? Have we shaved these yaks in vain?
No, we haven’t. rustler
provides us with the ResourceArc
, which the docs desribe as
thread-safe, reference-counted storage for Rust data that can be shared across threads…Rust code and Erlang code can both have references to the same resource at the same time. Rust code uses ResourceArc; in Erlang, a reference to a resource is a kind of term. You can convert back and forth between the two using Encoder and Decoder.
Which, in practical terms, boils down to allowing us to wrap a Rust struct in ResourceArc
and return that from a NIF. This will be returned to Elixir as an Elixir Reference
pointing to the Rust data in memory. Then, we can pass that reference back into a NIF and it will be dereferenced to the Rust resource.
So with a few changes, we can get a few steps closer. The full code for this step is available in a Gist here, but the relevant excerpts are shown below:
pub struct CairoWrapper {
pub context: Context,
pub surface: ImageSurface
}
type WrapperRA = ResourceArc<CairoWrapper>;
...
#[rustler::nif]
fn save(context: WrapperRA, filename: String) -> WrapperRA {
...
}
rustler::init!("Elixir.Xairo.Native", [...], load=on_load);
fn on_load(env: Env, _info: Term) -> bool {
rustler::resource!(CairoWrapper, env);
true
}
We define a custom struct that wraps our context and image surface (we’ll need a direct reference to the surface to save the image, as we saw in the failing example above). Then we create a type alias with our new struct wrapped in a ResourceArc
, which allows us to pass a resource to the object in memory back and forth between Rust and Elixir. We also change all our function signatures to accept and return our new wrapper type. And finally, we create a function that runs when the NIFs are loaded, and in it call the rustler::resource!
macro on our struct, which creates the functions necessary to allow ResourceArc
to wrap and pass our struct.
Over in Elixir, we need to wire this up to the module we’ve defined as the target, Xairo.Native
, like so. We just need the function signatures present so we can call them in Elixir, but the actual function bodies should never be called, as we can see by the fact that they all return an error.
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
We run mix compile
, and it finishes cleanly!
Let’s fire up iex -S mix
and see what we can do.
iex(1)> i = Xairo.Native.new(100, 100)
#Reference<0.3888755325.2009989122.47088>
iex(4)> Xairo.Native.paint(i, 0.5, 0.0, 1.0)
#Reference<0.3888755325.2009989122.47088>
iex(5)> Xairo.Native.save(i, "test.png")
#Reference<0.3888755325.2009989122.47088>
Well, we didn’t get any errors, and, as expected because of how rustler’s ResourceArc
works, we see that all these functions return an Elixir reference. In fact, we see they all return the same Elixir reference, because under the hood in Rust (and under that hood, in C), we’re modifying a mutable object in memory. This means that, unlike if we were writing pure Elixir code, we don’t need to reassign i
to the result of every operation.2
Because these functions do return a reference to the object in memory, we can still pipe them, turning the above IEx commands into
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>
Again, it all seems to work. But what, if anything, is saved in “test.png?” Let’s open it up and find out
Just as we’d hoped! A 100x100 pixel purple square, created in an IEx session, and saved to a file!
Seems like we accomplished the goal we set out at the beginning of the post, and with only a quite manageable amount of yak fur to show for it.
Next time, we’ll look into ways to make handling resource Reference
a bit more Elixir-y, and wrap our Xairo.Native
NIFs in a Xairo.Image
public API.
Until next time! Thanks for reading!
Footnotes
-
Intro to Cairo Graphics in Rust, for example, which dives a bit deeper into the specifics of the Rust syntax than I intend to. In fact, this blog post gives us pretty much all the Rust code we need to accomplish our goals for this blog post, so we will be drawing on it heavily for the Rust side of the library. ↩ ↩2
-
This also means that, technically, we can do some very un-Elixir things with
map
andfor
to modify the image. We can attempt to discourage this sort of behaviour via our final API design, but it will still be possible. Father Valim, full of grace, lead us not into mutable temptation… ↩