4 releases
0.1.0 | Mar 5, 2020 |
---|---|
0.0.3 | Jun 9, 2019 |
0.0.2 | Jun 8, 2019 |
0.0.1 | Jun 5, 2019 |
#926 in Encoding
62 downloads per month
56KB
1.5K
SLoC
serde_rustler
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.
Installation
Install from Crates.io:
[dependencies]
serde_rustler = "0.0.3"
Quick Start
#[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())
}
Usage
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
Conversion Table
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).
Benchmarks
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.
TODO
- finalize behaviour around chars, charlists, iolists, map keys
- still getting used to Rust, so may need to improve error handling and ergnomoics around API
- support for
i128
andu128
- more extensive (i.e. possible addition of smoke, property-based) testing
- investigate
decode_json
(Serializer?) performance degradation
Changelog
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 |
Contributing
- Fork it https://github.com/your_username/serde_rustler/fork
- Create your feature branch (
git checkout -b feature/fooBar
) - Commit your changes (
git commit -am 'Add some fooBar'
) - Push to the branch (
git push origin feature/fooBar
) - Create a new Pull Request
Maintainers
- Sunny G - @sunny-g
License
MIT
Dependencies
~2.8–4MB
~73K SLoC