Rustler 101: Ferris Say
Tags: elixir, rust
Published: 2020-09-17 15:44:21

สร้าง Elixir Project ด้วย mix

```
$ mix new ferris_ex --module FerrisEx
```

ใส่ `rustler` เข้าไปใน mix dependencies

```
defp deps do
  [
    {:rustler, "~> 0.21.1"}
  ]
end
```

หลังจากนั้นก็สั่ง `mix deps.get` เพื่อ fetch dependencies

ต่อมา generate rust project ผ่าน `rustler` ก็จะมี prompt ให้ใส่ชื่อ module ลงไป ผมใส่ module ชื่อ `FerrisEx.Native` และชื่อ rust library ก็ตามน้ำมันไป

```
$ mix rustler.new
==> toml
Compiling 10 files (.ex)
Generated toml app
==> rustler
Compiling 5 files (.ex)
Generated rustler app
==> ferris_ex
This is the name of the Elixir module the NIF module will be registered to.
Module name > FerrisEx.Native
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (ferrisex_native) >
* creating native/ferrisex_native/.cargo/config
* creating native/ferrisex_native/README.md
* creating native/ferrisex_native/Cargo.toml
* creating native/ferrisex_native/src/lib.rs
Ready to go! See /Users/thanabodee/src/github.com/wingyplus/ferris_ex/native/ferrisex_native/README.md for further instructions.
```

ต่อมาเปิด Cargo.toml ซึ่งอยู่ใน `$PWD/native/ferrisex_native/Cargo.toml` แล้วทำการใส่ library ชื่อ `ferris_says` ลงไป

```
[dependencies]
rustler = "0.21.1"
lazy_static = "1.0"
...
ferris-says = "0.2"
```

และทำการแก้โค้ดของ lib.rs ให้เป็น function ตามที่เราต้องการ

```rust
use ferris_says;
use rustler::{Encoder, Env, Error, Term};
use std::io;
use std::str;

mod atoms {
    rustler::rustler_atoms! {
        atom ok;
    }
} // 1

rustler::rustler_export_nifs! {
    "Elixir.FerrisEx.Native",
    [
        ("say", 1, say)
    ],
    None
} // 2

fn say<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let text: String = args[0].decode()?;

    let mut cursor = io::Cursor::new(Vec::new());
    ferris_says::say(&text.into_bytes(), 24, &mut cursor).unwrap();
    Ok((atoms::ok(), str::from_utf8(&cursor.into_inner()).unwrap()).encode(env))
} // 3
```

จาก snippet ข้างบน

1. ประกาศ atom ที่เราจะใช้ใน rust library
2. ประกาศ function ใน module `FerrisEx.Native`
3. implement function

เสร็จแล้วก็สั่ง `cargo build` เพื่อให้ rust สร้าง dynamic library ขึ้นมา

ต่อมาทำการสร้าง elixir module เพื่อ binding กับ rust library ของเรา

```
# ./lib/ferris_ex/native.ex
defmodule FerrisEx.Native do
  use Rustler, otp_app: :ferris_ex, crate: :ferrisex_native

  def say(_text), do: :erlang.nif_error(:nif_not_loaded)
end
```

เพื่อที่จะทำการ binding กับ rust library ของเราได้ต้องใช้ `use Rustler` และบอกว่าจะผูกกับ crate ชื่ออะไรของเรา และต้องมี function dummy เพื่อกรณีที่ elixir binding nif ไม่ได้

เสร็จแล้วก็ copy dynamic library ที่ได้จาก `cargo build` มาไว้ที่ `priv/native` ของ elixir 

```
$ cp ./native/ferrisex_native/target/debug/libferrisex_native.dylib ./priv/native/libferrisex_native.so
```

หลังจาก copy แล้วก็ทำการเปิด iex ขึ้นมาเพื่อทดสอบ binding ของเรา

```
iex(1)> {:ok, out} = FerrisEx.Native.say("Hello, I'm Ferris.")
{:ok,
 " ____________________\n< Hello, I'm Ferris. >\n --------------------\n        \\\n         \\\n            _~^~^~_\n        \\) /  o o  \\ (/\n          '_   -   _'\n          / '-----' \\\n"}
iex(2)> IO.puts(out)
 ____________________
< Hello, I'm Ferris. >
 --------------------
        \
         \
            _~^~^~_
        \) /  o o  \ (/
          '_   -   _'
          / '-----' \

:ok
```