serde_rustler
provides a Serde Serializer and Deserializer for Rustler types, so you can easily serialize and deserialize native Rust types directly to and from native Elixir terms within your NIFs.
Install from Crates.io:
toml
[dependencies]
serde_rustler = "0.0.3"
```rust
use serde::{Serialize, Deserialize} use serderustler::{fromterm, to_term};
rustlerexportnifs! { "Elixir.SerdeRustlerTests", [("nif", 1, nif)], None }
struct Animal = { ... };
fn nif<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult
// Serialize a type into an Elixir term.
to_term(env, animal).map_err(|err| err.into())
} ```
Below is a more comprehensive example of how you might use serde_rustler
within a rust NIF...
```rust
extern crate rustler;
use rustler::{Env, error::Error as NifError, NifResult, Term}; use serde::{Serialize, Deserialize}; use serderustler::{fromterm, to_term};
rustlerexportnifs! { "Elixir.SerdeNif", [("readme", 1, readme)], None }
// NOTE: to serialize to the correct Elixir record, you MUST tell serde to // rename the variants to the full Elixir record module atom.
enum AnimalType { #[serde(rename = "Elixir.SerdeNif.AnimalType.Cat")] Cat(String), #[serde(rename = "Elixir.SerdeNif.AnimalType.Dog")] Dog(String), }
// NOTE: to serialize to an actual Elixir struct (rather than a just map with // a :struct key), you MUST tell serde to rename the struct to the full // Elixir struct module atom.
struct Animal {
#[serde(rename = "type")]
_type: AnimalType,
name: String,
age: u8,
owner: Option
fn readme<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult
... and how you might structure your corresponding Elixir types (code structure, import
s, alias
es and require
s simplified or omitted for brevity):
```elixir defmodule SerdeNif do use Rustler, otpapp: :serdenif
def readme(term), do: :erlang.niferror(:nifnotloaded)
defmodule Animal do @type t :: %Animal{ type: Cat.t() | Dog.t(), name: bitstring, age: pos_integer, owner: nil | bitstring } defstruct type: Cat.record(), name: "", age: 0, owner: nil
@doc """
Deserializes term as a Rust `Animal` struct, then serializes it back into
an Elixir `Animal` struct. Should return true.
"""
def test() do
animal = %Animal{
type: Animal.Cat.record(),
name: "Garfield",
age: 41,
}
SerdeNif.readme(animal) == animal
end
end
defmodule AnimalType.Cat do require Record @type t {MODULE, String.t()} Record.defrecord(:record, MODULE, breed: "tabby") end
defmodule AnimalType.Dog do # omitted end end ```
| Type Name | Serde (Rust) Values | Elixir Terms (default behaviour) | deserialize_any
into Elixir Term |
|-----|-----|-----|-----|
| bool | true
or false
| true
or false
| true
or false
|
| 1 number | i8
, i16
, i32
, i64
, u8
, u16
, u32
, u64
, f32
, f64
(TODO: i128
and u128
) | number
| number
as f64
, i64
, or u64
|
| char | 'A'
| [u32]
| [u32]
|
| string | ""
| bitstring
| bitstring
|
| byte array | &[u8]
or Vec<u8>
| <<_::_*8>>
| bitstring
|
| option | Some(T)
or None
| T
or :nil
| T
or :nil
|
| unit | None
| :nil
| :nil
|
| unit struct | struct Unit
| :nil
| :nil
|
| 3 unit variant | E::A
in enum UnitVariant { A }
| :A
| "A"
|
| 3 newtype struct | struct Millimeters(u8)
| {:Millimeters, u8}
| ["Millimeters", u8]
|
| 3 newtype variant | E::N
in enum E { N(u8) }
| {:N, u8}
| ["N", u8]
|
| 3 newtype variant (any Ok
and Err
tagged enum) | enum R<T, E> { Ok(T), Err(E) }
| {:ok, T}
or {:error, E}
| ["Ok", T]
or ["Err", E]
|
| seq | Vec<T>
| [T,]
| [T,]
|
| tuple | (u8,)
| {u8,}
| [u8,]
|
| 3 tuple struct | struct Rgb(u8, u8, u8)
| {:Rgb, u8, u8, u8}
| ["Rgb", u8, u8, u8]
|
| 3 tuple variant | E::T
in enum E { T(u8, u8) }
| {:T, u8, u8}
| ["T", u8, u8]
|
| 1 map | HashMap<K, V>
| %{}
| %{}
|
| 3 struct | struct Rgb { r: u8, g: u8, b: u8 }
| %Rgb{ r: u8, g: u8, b: u8 }
| %{"r" => u8, "g" => u8, "b" => u8}
|
| 3 struct variant | E::S
in enum E { Rgb { r: u8, g: u8, b: u8 } }
| %Rgb{ r: u8, g: u8, b: u8 }
| %{"r" => u8, "g" => u8, "b" => u8}
|
1: API still being decided / implemented.
2: When serializing unknown input to terms, atoms will not be created and will instead be replaced with Elixir bitstrings. Therefore "records" will be tuples ({bitstring, ...}
) and "structs" will be maps containing %{:__struct__ => bitstring}
. The unfortunate consequence of this is that deserialize_any
will lack the necessary information needed deserialize many terms without type hints, such as structs
, enums
and enum variants
, and tuples
. (Feedback on how best to solve this is very welcome here).
To run:
sh
cd serde_rustler_tests
MIX_ENV=bench mix run test/benchmarks.exs
Benchmarks were ripped from the Poison repo. The NIFs being called were implemented using serde-transcode
to translate between serde_rustler
and serde_json
and were compiled in :release
mode by rustler
.
NOTE: If someone can point out any mistakes I made that led to these ridiculous results, please let me know :)
Benchmarks suggest that serde_rustler
is somewhat faster than jiffy
when encoding JSON, and generally comparable to / no more than ~2-3x as slow as jiffy
or jason
when decoding JSON, and in almost all cases, serde_rustler
seems to use significantly less memory than pure-Elixir alternatives, though this is likely has to do with running a NIF rather than an pure-Elixir function.
Also take note of the results for any test taking longer than 1ms or tests involving the larger inputs govtrack.json
(3.74 MB) and issue-90.json
(7.75 MB) - the encode_json_compact
and decode_json
NIFs have significantly higher variation in performance while their dirty equivalents encode_json_compact_dirty
and decode_json_dirty
are comparable to the originals in speed and have more reliable performance.
i128
and u128
decode_json
(Serializer?) performance degradation| Version | Change Summary |
| ------- | ---------------|
| v0.0.3 | better char
and tuple
support, adds benchmarks |
| v0.0.2 | cleanup, better deserialize_any
support |
| v0.0.1 | initial release |
git checkout -b feature/fooBar
)git commit -am 'Add some fooBar'
)git push origin feature/fooBar
)MIT