Bind me if you can
Răzvan Rotari
Cristian Neamțu
How will you transform JSON into
an object ?
C++
{
"a": "1",
"b": 2,
"c": 3.0
}
struct SimpleStruct {
std::string a;
int b;
double c;
};
auto j =
nlohmann::json::parse(jsonString);
SimpleStruct s;
s.a = j[“a”];
s.b = j[“b”];
s.c = j[“c”];
Rust
{
"a": "1",
"b": 2,
"c": 3.0
}
#[derive(Serialize, Deserialize)]
struct Example {
a: String,
b: i32,
c: f64
}
let e: Example = serde_json::from_str(data)?;
Can we reproduce this
in c++?
NO!
But how close can we get?
Idea 1- Pass object description as parameter
const auto description = std::make_tuple(
std::make_pair("a", &SimpleStruct::a),
std::make_pair("b", &SimpleStruct::b),
std::make_pair("c", &SimpleStruct::c));
const auto output = readJson<SimpleStruct>(jsonData, description);
template <typename ReturnType, typename... Tuple>
ReturnType readJson(std::string_view jsonString, const std::tuple&
description) {
ReturnType ret;
auto j = nlohmann::json::parse(jsonString);
auto readValue = [&](const auto& key) { return j[key]; };
auto populateObject = [&](const auto& elemDesc) {
auto ReturnType::*ptr = std::get<1>(elemDesc);
ret.*ptr = readValue(std::get<0>(elemDesc));
};
std::apply([&](auto&... x) { (..., populateObject(x)); }, description);
/* [&](std::pair<char*, int SimpleStruct::*>& x1,
std::pair<char*, std::string SimpleStruct::*>& x2, ...) {
(populateObject(x1), populateObject(x2), populateObject(x3));
} */
return ret;
}
Idea 1- Pass object description as parameter
- No direct link between object and description
- Boilerplate
- Does not exposes implementation details
Idea 2- Automatic description detection
template <>
struct Descriptor {
static auto getDescription() {
return std::make_tuple(
std::make_pair("a", &SimpleStruct::a),
std::make_pair("b", &SimpleStruct::b),
std::make_pair("c", &SimpleStruct::c));
}
};
const auto output = readJson<SimpleStruct>(jsonData);
template <typename ReturnType>
struct Descriptor {
static void getDescription() {}
};
template <typename ReturnType>
ReturnType readJson(std::string_view jsonString) {
ReturnType ret;
auto j = json::parse(jsonString);
auto populateObject = /*Same as above*/
const auto description = Descriptor<ReturnType>{}.getDescription();
std::apply([&](auto&... x) { (..., populateObject(x)); }, description);
return ret;
}
Idea 2- Automatic description detection
- Even more boilerplate
- Longer compilation times
- Simpler interface
- Does not exposes implementation details
Idea 3 - Embed description into a type
//Requires c++20
template <>
struct Descriptor {
using type = std::tuple<
SETTER("a", &SimpleStruct::a),
SETTER("b", &SimpleStruct::b),
SETTER("c", &SimpleStruct::c)>;
};
const auto output = readJson<SimpleStruct>(jsonData);
template <>
struct Descriptor {
using type =
std::tuple<std::pair<decltype([]() { return "a"; }),
decltype(setter<&SimpleStruct::a>())>,
std::pair<decltype([]() { return "b"; }),
decltype(setter<&SimpleStruct::b>())>,
std::pair<decltype([]() { return "c"; }),
decltype(setter<&SimpleStruct::c>())> >;
};
EXPANDED VERSION
setter() function
template<auto PtrToMember>
constexpr auto setter() {
using Def = PtrMemberExtractor<PtrToMember>;
constexpr auto setterf = [](Def::containing_type & obj,
const Def::result& value) constexpr {
obj.*(Def::value) = value;
};
return setterf;
}
template <typename Class, typename Result, Result Class::*Value>
struct PtrMemberExtractor {
using containing_type = Class;
using result = Result;
static const auto value = Value;
};
template <typename ReturnType>
ReturnType readJson(std::string_view jsonString) {
ReturnType ret;
const auto j = json::parse(jsonString);
auto populateObject = [&](const auto& elemDesc) {
auto setter = std::get<1>(elemDesc);
auto keyFunc = std::get<0>(elemDesc);
setter(ret, j[(keyFunc())]);
};
const typename Descriptor<ReturnType>::type desc;
std::apply([&](auto&... x) { (..., populateObject(x)); }, desc);
/*The above expands to this
[&](std::pair<decltype([]() { return "a"; }),
decltype(setter<&SimpleStruct::a>())> x1,...){
(populateObject(x1), populateObject(x2), populateObject(x3));
} */
return ret;
}
Idea 3 - Embed description into a type
- Uses macros
- Even longer compilation times
- Requires c++20
- Does not exposes implementation details
-
Performace
Baseline Parameter Automatic Compile
mean 2.631 us
Low mean 2.623 us
High mean 2.656 us
Conclusions
- Good to hide implementation
- In practice the keys need to be provided so the boilerplate is reduced
- Works better for complex data, like the output of a SQL JOIN
- Serialization can use the same description
- Flexible pattern
Rust
What  Why Rust?
● Focused on memory safety
● No classes, no inheritance
● Traits over Interfaces
● No variadics in general
● Types can be deduced implicitly
● macro_rules! and procedural macros
The Ownership system
1. Each value has an owner
2. There can be only one owner at a time
3. When the owner goes out of scope the value gets dropped
The compiler:
● checks if the rules are respected
● checks the lifetimes of the data
● inserts clean up code during compilation
Code Examples
Traits
● similar to the idea of interfaces with some differences
● define a contract that must be supported by the desired data type
● can define include both static and instance specific methods
● can provide default implementations
● can be attached to any datatype (owned code standard-library 3rd-party)
Code Examples
macro_rules!
macro_rules! map {
( $($key:expr => $value:expr),* ) => {{
let mut map = HashMap::new();
$(map.insert($key, $value); )*
map
}};
}
fn main() {
let person = map!(
"name" => "John",
"surname" => "Brown"
);
println!("{:?}", person);
}
● Pattern matched
● Token-type safe
○ ty - types
○ ident - identifiers
○ expr - expressions
○ etc.
● Steep learning curve
● Must produce syntactically correct code
● Rust Playground Link
Procedural macros
● a compiler plugin written in Rust that is loaded during compilation
○ it receives the (method, struct, etc.) AST as a parameter
○ produces another AST (rust syntax)
struct SimpleVisitor;
impl Visitor for SimpleVisitor {
type Value = Simple;
type Error = BindError;
fn visit_map(self, mut map: MapAccess) -> Result<Simple, BindError> {
Ok(Simple {
a: map.get_value("a")?.unwrap(),
b: map.get_value("b")?.unwrap(),
c: map.get_value("c")?.unwrap(),
})
}
}
How can I get from this?
To this?
#[derive(Deserializable,Debug)]
pub struct Simple {
pub a: i32,
pub b: String,
pub c: f64,
}
ProcMacro - TokenStream
pub[Ident] struct[Ident] Simple[Ident]
[Group]
pub[Ident] a[Ident] :[Punct] i32[Ident] ,[Punct]
pub[Ident] b[Ident] :[Punct] String[Ident] ,[Punct]
pub[Ident] c[Ident] :[Punct] f64[Ident] ,[Punct]
Implementation
Getting into Serde
● Main Traits
● Examples && Unit Tests
● Macros
Conclusions
● No boilerplate (due to the powerful macro system)
○ Serde: Extensive configurability (renaming, transforming or ignoring attributes, etc.)
○ Type safe conversions
● Serde efficiency due zero-copy deserialization
○ Benchmark - comparison
● Serde has been widely adopted as the “standard” for serialization and
deserialization in Rust:
○ https://serde.rs/#data-formats (json, yaml, csv, etc.)
Implementations available at:
● https://github.com/RazvanRotari/binding_experiment
Thank you!
Questions?

Bind me if you can

  • 1.
    Bind me ifyou can Răzvan Rotari Cristian Neamțu
  • 2.
    How will youtransform JSON into an object ?
  • 3.
    C++ { "a": "1", "b": 2, "c":3.0 } struct SimpleStruct { std::string a; int b; double c; }; auto j = nlohmann::json::parse(jsonString); SimpleStruct s; s.a = j[“a”]; s.b = j[“b”]; s.c = j[“c”];
  • 4.
    Rust { "a": "1", "b": 2, "c":3.0 } #[derive(Serialize, Deserialize)] struct Example { a: String, b: i32, c: f64 } let e: Example = serde_json::from_str(data)?;
  • 5.
    Can we reproducethis in c++?
  • 6.
  • 7.
    But how closecan we get?
  • 8.
    Idea 1- Passobject description as parameter const auto description = std::make_tuple( std::make_pair("a", &SimpleStruct::a), std::make_pair("b", &SimpleStruct::b), std::make_pair("c", &SimpleStruct::c)); const auto output = readJson<SimpleStruct>(jsonData, description);
  • 9.
    template <typename ReturnType,typename... Tuple> ReturnType readJson(std::string_view jsonString, const std::tuple& description) { ReturnType ret; auto j = nlohmann::json::parse(jsonString); auto readValue = [&](const auto& key) { return j[key]; }; auto populateObject = [&](const auto& elemDesc) { auto ReturnType::*ptr = std::get<1>(elemDesc); ret.*ptr = readValue(std::get<0>(elemDesc)); }; std::apply([&](auto&... x) { (..., populateObject(x)); }, description); /* [&](std::pair<char*, int SimpleStruct::*>& x1, std::pair<char*, std::string SimpleStruct::*>& x2, ...) { (populateObject(x1), populateObject(x2), populateObject(x3)); } */ return ret; }
  • 10.
    Idea 1- Passobject description as parameter - No direct link between object and description - Boilerplate - Does not exposes implementation details
  • 11.
    Idea 2- Automaticdescription detection template <> struct Descriptor { static auto getDescription() { return std::make_tuple( std::make_pair("a", &SimpleStruct::a), std::make_pair("b", &SimpleStruct::b), std::make_pair("c", &SimpleStruct::c)); } }; const auto output = readJson<SimpleStruct>(jsonData);
  • 12.
    template <typename ReturnType> structDescriptor { static void getDescription() {} }; template <typename ReturnType> ReturnType readJson(std::string_view jsonString) { ReturnType ret; auto j = json::parse(jsonString); auto populateObject = /*Same as above*/ const auto description = Descriptor<ReturnType>{}.getDescription(); std::apply([&](auto&... x) { (..., populateObject(x)); }, description); return ret; }
  • 13.
    Idea 2- Automaticdescription detection - Even more boilerplate - Longer compilation times - Simpler interface - Does not exposes implementation details
  • 14.
    Idea 3 -Embed description into a type //Requires c++20 template <> struct Descriptor { using type = std::tuple< SETTER("a", &SimpleStruct::a), SETTER("b", &SimpleStruct::b), SETTER("c", &SimpleStruct::c)>; }; const auto output = readJson<SimpleStruct>(jsonData);
  • 15.
    template <> struct Descriptor{ using type = std::tuple<std::pair<decltype([]() { return "a"; }), decltype(setter<&SimpleStruct::a>())>, std::pair<decltype([]() { return "b"; }), decltype(setter<&SimpleStruct::b>())>, std::pair<decltype([]() { return "c"; }), decltype(setter<&SimpleStruct::c>())> >; }; EXPANDED VERSION
  • 16.
    setter() function template<auto PtrToMember> constexprauto setter() { using Def = PtrMemberExtractor<PtrToMember>; constexpr auto setterf = [](Def::containing_type & obj, const Def::result& value) constexpr { obj.*(Def::value) = value; }; return setterf; } template <typename Class, typename Result, Result Class::*Value> struct PtrMemberExtractor { using containing_type = Class; using result = Result; static const auto value = Value; };
  • 17.
    template <typename ReturnType> ReturnTypereadJson(std::string_view jsonString) { ReturnType ret; const auto j = json::parse(jsonString); auto populateObject = [&](const auto& elemDesc) { auto setter = std::get<1>(elemDesc); auto keyFunc = std::get<0>(elemDesc); setter(ret, j[(keyFunc())]); }; const typename Descriptor<ReturnType>::type desc; std::apply([&](auto&... x) { (..., populateObject(x)); }, desc); /*The above expands to this [&](std::pair<decltype([]() { return "a"; }), decltype(setter<&SimpleStruct::a>())> x1,...){ (populateObject(x1), populateObject(x2), populateObject(x3)); } */ return ret; }
  • 18.
    Idea 3 -Embed description into a type - Uses macros - Even longer compilation times - Requires c++20 - Does not exposes implementation details -
  • 19.
    Performace Baseline Parameter AutomaticCompile mean 2.631 us Low mean 2.623 us High mean 2.656 us
  • 20.
    Conclusions - Good tohide implementation - In practice the keys need to be provided so the boilerplate is reduced - Works better for complex data, like the output of a SQL JOIN - Serialization can use the same description - Flexible pattern
  • 21.
  • 22.
    What WhyRust? ● Focused on memory safety ● No classes, no inheritance ● Traits over Interfaces ● No variadics in general ● Types can be deduced implicitly ● macro_rules! and procedural macros
  • 23.
    The Ownership system 1.Each value has an owner 2. There can be only one owner at a time 3. When the owner goes out of scope the value gets dropped The compiler: ● checks if the rules are respected ● checks the lifetimes of the data ● inserts clean up code during compilation Code Examples
  • 24.
    Traits ● similar tothe idea of interfaces with some differences ● define a contract that must be supported by the desired data type ● can define include both static and instance specific methods ● can provide default implementations ● can be attached to any datatype (owned code standard-library 3rd-party) Code Examples
  • 25.
    macro_rules! macro_rules! map { ($($key:expr => $value:expr),* ) => {{ let mut map = HashMap::new(); $(map.insert($key, $value); )* map }}; } fn main() { let person = map!( "name" => "John", "surname" => "Brown" ); println!("{:?}", person); } ● Pattern matched ● Token-type safe ○ ty - types ○ ident - identifiers ○ expr - expressions ○ etc. ● Steep learning curve ● Must produce syntactically correct code ● Rust Playground Link
  • 26.
    Procedural macros ● acompiler plugin written in Rust that is loaded during compilation ○ it receives the (method, struct, etc.) AST as a parameter ○ produces another AST (rust syntax)
  • 27.
    struct SimpleVisitor; impl Visitorfor SimpleVisitor { type Value = Simple; type Error = BindError; fn visit_map(self, mut map: MapAccess) -> Result<Simple, BindError> { Ok(Simple { a: map.get_value("a")?.unwrap(), b: map.get_value("b")?.unwrap(), c: map.get_value("c")?.unwrap(), }) } } How can I get from this?
  • 28.
    To this? #[derive(Deserializable,Debug)] pub structSimple { pub a: i32, pub b: String, pub c: f64, }
  • 29.
    ProcMacro - TokenStream pub[Ident]struct[Ident] Simple[Ident] [Group] pub[Ident] a[Ident] :[Punct] i32[Ident] ,[Punct] pub[Ident] b[Ident] :[Punct] String[Ident] ,[Punct] pub[Ident] c[Ident] :[Punct] f64[Ident] ,[Punct] Implementation
  • 30.
    Getting into Serde ●Main Traits ● Examples && Unit Tests ● Macros
  • 32.
    Conclusions ● No boilerplate(due to the powerful macro system) ○ Serde: Extensive configurability (renaming, transforming or ignoring attributes, etc.) ○ Type safe conversions ● Serde efficiency due zero-copy deserialization ○ Benchmark - comparison ● Serde has been widely adopted as the “standard” for serialization and deserialization in Rust: ○ https://serde.rs/#data-formats (json, yaml, csv, etc.)
  • 33.
    Implementations available at: ●https://github.com/RazvanRotari/binding_experiment
  • 34.