Crates.io | serde_rustler |
lib.rs | serde_rustler |
version | 0.1.0 |
source | src |
created_at | 2019-06-05 11:50:17.71468 |
updated_at | 2020-03-05 17:59:24.97189 |
description | Serde Serializer and Deserializer for Rustler NIFs |
homepage | |
repository | https://github.com/sunny-g/serde_rustler |
max_upload_size | |
id | 139130 |
size | 55,781 |
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:
[dependencies]
serde_rustler = "0.0.3"
#[macro_use] extern crate rustler;
use serde::{Serialize, Deserialize}
use serde_rustler::{from_term, to_term};
rustler_export_nifs! {
"Elixir.SerdeRustlerTests",
[("nif", 1, nif)],
None
}
#[derive(Serialize, Deserialize)]
struct Animal = { ... };
fn nif<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult<Term<'a>> {
// Deserialize term into a native Rust type.
let animal: Animal = from_term(args[0])?;
// 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...
#[macro_use]
extern crate rustler;
use rustler::{Env, error::Error as NifError, NifResult, Term};
use serde::{Serialize, Deserialize};
use serde_rustler::{from_term, to_term};
rustler_export_nifs! {
"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.
#[derive(Debug, Serialize, Deserialize)]
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.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename = "Elixir.SerdeNif.Animal")]
struct Animal {
#[serde(rename = "type")]
_type: AnimalType,
name: String,
age: u8,
owner: Option<String>,
}
fn readme<'a>(env: Env<'a>, args: &[Term<'a>]) -> NifResult<Term<'a>> {
let animal: Animal = from_term(args[0])?;
println!("serialized animal: {:?}", animal);
to_term(env, animal).map_err(|err| err.into())
}
... and how you might structure your corresponding Elixir types (code structure, import
s, alias
es and require
s simplified or omitted for brevity):
defmodule SerdeNif do
use Rustler, otp_app: :serde_nif
def readme(_term), do: :erlang.nif_error(:nif_not_loaded)
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:
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 degradationVersion | 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