This crate provides an example of how to use the Rust Sv2 Decoder
and Encoder
from C++.
To run the example: ./run.sh
.
The example is composed by a Rust "downstream node" that keep sending a
[common_messages_sv2::SetupConnection
] message to a C++ "upstream node" that receive the message
and keep answering with a [common_messages_sv2::SetupConnectionError
].
The Rust codec is exported as a C static library by the crate sv2-ffi.
This crate also provide an example of how to build
the sv2-ffi
as a static library using guix.
The header file is generated with cbindgen
.
Rust enums definition are transformed by cbingen
in:
struct [Rust_enum_name] {
union class Tag {
[union_element_1_name]
[union_element_2_name]
...
}
struct [union_element_1_name]_Body {
[inner_union_element_name_if_any_1] _0;
[inner_union_element_name_if_any_2] _1;
...
}
struct [union_element_2_name]_Body {
[inner_union_element_name_if_any_1] _0;
[inner_union_element_name_if_any_2] _1;
...
}
...
union {
[union_element_1_name]_Body [union_element_1_name]
[union_element_2_name]_Body [union_element_2_name]
...
}
}
For example the below Rust enum:
#[repr(C)]
pub enum CResult<T, E> {
Ok(T),
Err(E),
}
Will be transformed in:
template<typename T, typename E>
struct CResult {
enum class Tag {
Ok,
Err,
};
struct Ok_Body {
T _0;
};
struct Err_Body {
E _0;
};
Tag tag;
union {
Ok_Body ok;
Err_Body err;
};
};
All the memory used shared struct/enums (also when borrowed) is allocated by Rust.
When C++ take ownership of a Sv2 message the message must be manually dropped.
In order to pattern match against a Rust defined enum from C++:
CResult < CSv2Message, Sv2Error > frame = next_frame(decoder);
switch (frame.tag) {
case CResult < CSv2Message, Sv2Error > ::Tag::Ok:
on_success(frame.ok._0);
cout << "\n";
cout << "START PARSING NEW FRAME";
cout << "\n";
send_setup_connection_error(new_socket);
break;
case CResult < CSv2Message, Sv2Error > ::Tag::Err:
on_error(frame.err._0);
break;
};
[binary_sv2::binary_codec_sv2::CVec
] is used to share bytes buffers between Rust and C++.
A CVec
can be either "borrowed" or "owned" if is on or the other depend by the method that we
use to construct it.
- (borrowed) [
binary_sv2::binary_codec_sv2::CVec::as_shared_buffer
]: used when we need to fill a Rust allocated buffer from C++. This method does not guarantee anything about the pointed memory and the user must enforce that the Rust side does not free the pointed memory while the C++ part is using it ACVec
constructed with this method must not be freed by C++ (this is enforced by the fact that the function used to free theCVec
is not exported in the C library) - (owned)
&[u8].into::<CVec>()
: used to copy the contents of the&[u8]
to aCVec
. It must be dropped from C++ via [sv2_ffi::drop_sv2_message
] - (owned) [
binary_sv2::binary_codec_sv2::cvec_from_buffer
]: used when aCVec
must be created in C++, is used to construct a [sv2_ffi::CSv2Message
] that will be dropped as usual with [sv2_ffi::drop_sv2_message
] - (owned)
CVec2.into::<Vec<CVec>>()
, seeCVec2
section - (owned)
Inner.into::<CVec>()
: same as&[u8].into::<CVec>()
A CVec2
is a vector of CVec
's. It is always allocated in Rust, is used only as field of Sv2 messages, and is
dropped when the Sv2 message gets dropped.
[sv2_ffi::DecoderWrapper
] is instantiated in C++ via [sv2_ffi::new_decoder
].
There is no need to drop it as it will live for the entire life of the program.
[sv2_ffi::get_writable
] returns a CVec
and is Rust allocated memory that C++ can fill with the
socket content. The CVec
is "borrowed" (&[u8].into::<CVec>()
) so it will be automatically
dropped by Rust.
[sv2_ffi::next_frame
] is used if a complete Sv2 frame is available, it returns a [sv2_ffi::CSv2Message
].
The message can contain one or more "owned" CVec
's, so it must be manually dropped via
[sv2_ffi::drop_sv2_message
].
[sv2_ffi::EncoderWrapper
] is instantiated in C++ via [sv2_ffi::new_encoder
].
There is no need to drop it as it will live for the entire life of the program.
A [sv2_ffi::CSv2Message
] can be constructed in C++ (here is an example)
if the message contains one or more CVec
's, then the content of the CVec
must be copied in a Rust allocated
CVec
with [binary_sv2::binary_codec_sv2::cvec_from_buffer
]. The message must be dropped with
[sv2_ffi::drop_sv2_message
].
[sv2_ffi::encode
] encodes a [sv2_ffi::CSv2Message
] as an encoded Sv2 frame in a buffer internal
to [sv2_ffi::EncoderWrapper
]. The buffer contents are returned as a "borrowed" CVec
. After
that, C++ has copied it and it must free the encoder with [sv2_ffi::flush_encoder
].
This is necessary because the encoder will reuse the internal buffer to encode the next message with
[sv2_ffi::flush_encoder
]. We let the encoder know that the content of the internal buffer has been copied
and can be overwritten.
- Instantiate a decoder with [
sv2_ffi::new_decoder
] - Fill the decoder, copying the input bytes in the buffer returned by [
sv2_ffi::get_writable
] - If the above buffer is full, call [
sv2_ffi::next_frame
]. If the decoder has enough bytes to decode an Sv2 frame it will return aCSv2Message
, otherwise it returns 2.
- Instantiate an encoder with [
sv2_ffi::new_encoder
] - Call [
sv2_ffi::encode
] with a validCSv2Message
- Copy the returned encoded frame where needed
- Call [
sv2_ffi::flush_encoder
] to let the encoder know that the encoded frame has been copied
An example of how to build a C++ program that use [sv2_ffi
] can be found
here.
The build script does the following:
- Packages
sv2_ffi
and all its dependencies (this step wont be necessary as the packages will be available in crates.io or github.com) - Calls g++ in a guix container defined here
The first 255 lines of example.scm
are a copy paste of the
guix cargo build system.
The only difference is that it uses Rust-1.51 as it needs const
generics basic support (line 30 of the manifest)
The actual manifest begins on L256, where it builds all the sv2_ffi
dependencies and then builds sv2_ffi
.
sv2_ffi
is a library crate and the guix default behavior is to not install the Rust library such that the
installation phase of sv2_ffi
is replaced and sv2.h
and the newly built libsv2_ffi.a
are installed
in the container (they are installed in /gnu/store/[hash]-Rust-sv2_ffi-[version]/
).
The manifest it expect to find sv2.h
in the sv2_ffi
package. Since the sv2.h
is created manually with
/build_header.sh
, it is very easy to commit code with an out of date header file. To ensure all commits include
the most updated header file, a GitHub Actions check is planned to be added.
run.sh
will (indirectly) install cbindgen for you or you can manually
$ cargo install cbindgen --force bts