C++ FFI (#4348)
* first commit in monorepo * *formatting *added license * Fix up license headers --------- Co-authored-by: mfahampshire <mfahampshire@pm.me> Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -128,7 +128,7 @@ default-members = [
|
||||
"nym-validator-rewarder",
|
||||
]
|
||||
|
||||
exclude = ["explorer", "contracts", "nym-wallet", "nym-connect/mobile/src-tauri", "nym-connect/desktop", "nym-vpn/ui/src-tauri", "cpu-cycles"]
|
||||
exclude = ["explorer", "contracts", "nym-wallet", "nym-connect/mobile/src-tauri", "nym-connect/desktop", "nym-vpn/ui/src-tauri", "cpu-cycles", "sdk/ffi/cpp"]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Nym Technologies SA"]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
src/main
|
||||
target/
|
||||
Generated
+5728
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "nym_cpp_ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "nym_cpp_ffi"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
# Nym clients, addressing, packet format, common tools (logging)
|
||||
nym-sdk = { git = "https://github.com/nymtech/nym", branch = "master" }
|
||||
nym-bin-common = { git = "https://github.com/nymtech/nym", branch = "master" }
|
||||
nym-sphinx-anonymous-replies = { git = "https://github.com/nymtech/nym", branch = "master" }
|
||||
# static var macro
|
||||
lazy_static = "1.4.0"
|
||||
# error handling
|
||||
anyhow = "1.0.75"
|
||||
# base58 en/decoding
|
||||
bs58 = "0.5.0"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# C++ FFI
|
||||
> ⚠️ This is an initial version of this library in order to give developers something to experiment with. If you use this code to begin testing out Mixnet integration and run into issues, errors, or have feedback, please feel free to open an issue; feedback from developers trying to use it will help us improve it. If you have questions feel free to reach out via our [Matrix channel]().
|
||||
|
||||
This repo contains:
|
||||
* `lib.rs`: an initial version of bindings for interacting with the Mixnet via the Rust SDK from C++.
|
||||
* `main.cpp`: an example of using this library, relying on `Boost` for threads.
|
||||
|
||||
The example `.cpp` file is a simple example flow of:
|
||||
* setting up Nym client logging
|
||||
* creating an ephemeral Nym client (no key storage / persistent address - this will come in a future iteration)
|
||||
* getting its [Nym address](https://nymtech.net/docs/clients/addressing-system.html)
|
||||
* using that address to send a message to yourself via the Mixnet
|
||||
* listen for and parse the incoming message for the `sender_tag` used for [anonymous replies with SURBs](https://nymtech.net/docs/architecture/traffic-flow.html#private-replies-using-surbs)
|
||||
* send a reply to yourself using SURBs
|
||||
|
||||
## Installation
|
||||
Prerequisites:
|
||||
* Rust
|
||||
* C++
|
||||
* [Boost](https://www.boost.org/) which can be installed with:
|
||||
```
|
||||
# Arch / Manjaro
|
||||
yay -S boost boost-libs
|
||||
|
||||
# Debian / Ubuntu
|
||||
sudo apt install libboost-all-dev
|
||||
```
|
||||
|
||||
## Usage
|
||||
The `build.sh` script in the root of the repository speeds up the task of building and linking the Rust and C++ code.
|
||||
* if want to quickly recompile your code run it as-is with `./build.sh`
|
||||
* if you want to clean build both the Rust and C++ code after removing existing compiled binaries run it with the optional `clean` argument: `./build.sh clean`.
|
||||
|
||||
> Make sure to run the script from the root of the project directory.
|
||||
|
||||
This script will:
|
||||
* (optionally if called with `clean` argument) remove existing Rust and C++ artifacts
|
||||
* build `lib.rs` with the `--release` flag
|
||||
* compile `main.cpp`, linking `lib.rs`
|
||||
* set value of `LD_LIBRARY_PATH` to the Rust code in `target/release/`
|
||||
* run the compiled `main`
|
||||
|
||||
## Error Handling
|
||||
When calling a function across the FFI boundary (e.g.) `reply`, the Rust code is matching the output of an `_internal` function - `Res` or `Err` - to a member of the `StatusCode` enum. This allows for both Rust-style error handling and the ease of returning a `c_int` across the FFI boundary, which can be used by C++ for its own error handling / conditional logic.
|
||||
|
||||
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
C_CODE_NAME="main"
|
||||
PROJECT_NAME="cpp"
|
||||
|
||||
clean_artifacts() {
|
||||
printf "cleaning cargo artifacts \n"
|
||||
cargo clean
|
||||
|
||||
if [ -e "src/${C_CODE_NAME}" ]
|
||||
then
|
||||
printf "cleaning compiled C++ \n"
|
||||
rm src/${C_CODE_NAME}
|
||||
else
|
||||
printf "no compiled C++ to clean \n"
|
||||
fi
|
||||
}
|
||||
|
||||
build_artifacts_and_link() {
|
||||
cargo build --release &&
|
||||
cd src/ &&
|
||||
printf "compiling cpp \n"
|
||||
g++ -std=c++11 -o main main.cpp -ldl -lpthread -L../target/release -lnym_cpp_ffi -lboost_thread &&
|
||||
export LD_LIBRARY_PATH=../target/release:$LD_LIBRARY_PATH
|
||||
# check output for name of rust lib - can be helpful if you've changed e.g. the name of a file and the compilation is failing
|
||||
# printf "ldd main: \n"
|
||||
# ldd main
|
||||
}
|
||||
|
||||
if [ $(pwd | awk -F/ '{print $NF}') != ${PROJECT_NAME} ]
|
||||
then
|
||||
printf "please run from root dir of project"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $# -eq 0 ];
|
||||
then
|
||||
build_artifacts_and_link;
|
||||
./main;
|
||||
else
|
||||
arg=$1
|
||||
if [ "$arg" == "clean" ]; then
|
||||
clean_artifacts;
|
||||
build_artifacts_and_link;
|
||||
./main;
|
||||
else
|
||||
echo "unknown optional argument - the only available optional argument is 'clean'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use lazy_static::lazy_static;
|
||||
use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, ReconstructedMessage};
|
||||
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::ffi::{c_char, c_int, CStr, CString};
|
||||
use std::mem::forget;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
/*
|
||||
NYM_CLIENT: Static reference (only init-ed once) to:
|
||||
- Arc: share ownership
|
||||
- Mutex: thread-safe way to share data between threads
|
||||
- Option: init-ed or not
|
||||
RUNTIME: Tokio runtime: no need to pass back to C and deal with raw pointers as it was previously
|
||||
*/
|
||||
lazy_static! {
|
||||
static ref NYM_CLIENT: Arc<Mutex<Option<MixnetClient>>> = Arc::new(Mutex::new(None));
|
||||
static ref RUNTIME: Runtime = Runtime::new().unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StatusCode {
|
||||
NoError = 0,
|
||||
ClientInitError = -1,
|
||||
ClientUninitialisedError = -2,
|
||||
SelfAddrError = -3,
|
||||
SendMsgError = -4,
|
||||
ReplyError = -5,
|
||||
ListenError = -6,
|
||||
}
|
||||
|
||||
// pub type CIntCallback = extern "C" fn(i32);
|
||||
pub type CStringCallback = extern "C" fn(*const c_char);
|
||||
pub type CMessageCallback = extern "C" fn(ReceivedMessage);
|
||||
|
||||
// FFI-sanitised way of sending back a ReconstructedMessage to C
|
||||
#[repr(C)]
|
||||
pub struct ReceivedMessage {
|
||||
message: *const u8,
|
||||
size: usize,
|
||||
sender_tag: *const c_char,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init_logging() {
|
||||
nym_bin_common::logging::setup_logging();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init_ephemeral() -> c_int {
|
||||
match init_ephemeral_internal() {
|
||||
Ok(_) => StatusCode::NoError as c_int,
|
||||
Err(_) => StatusCode::ClientInitError as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_ephemeral_internal() -> anyhow::Result<(), anyhow::Error> {
|
||||
if NYM_CLIENT.lock().unwrap().as_ref().is_some() {
|
||||
bail!("client already exists");
|
||||
} else {
|
||||
RUNTIME.block_on(async move {
|
||||
let init_client = MixnetClient::connect_new().await?;
|
||||
let mut client = NYM_CLIENT.try_lock();
|
||||
if let Ok(ref mut client) = client {
|
||||
**client = Some(init_client);
|
||||
} else {
|
||||
anyhow!("couldnt lock NYM_CLIENT");
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_self_address(callback: CStringCallback) -> c_int {
|
||||
match get_self_address_internal(callback) {
|
||||
Ok(_) => StatusCode::NoError as c_int,
|
||||
Err(_) => StatusCode::SelfAddrError as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_self_address_internal(callback: CStringCallback) -> anyhow::Result<(), anyhow::Error> {
|
||||
let client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
|
||||
if client.is_none() {
|
||||
bail!("Client is not yet initialised");
|
||||
}
|
||||
let nym_client = client
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
|
||||
// get address as cstring
|
||||
let c_string = CString::new(nym_client.nym_address().to_string())?;
|
||||
// as_ptr() keeps ownership in rust unlike into_raw() so no need to free it
|
||||
callback(c_string.as_ptr());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn send_message(recipient: *const c_char, message: *const c_char) -> c_int {
|
||||
match send_message_internal(recipient, message) {
|
||||
Ok(_) => StatusCode::NoError as c_int,
|
||||
Err(_) => StatusCode::SendMsgError as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_message_internal(
|
||||
recipient: *const c_char,
|
||||
message: *const c_char,
|
||||
) -> anyhow::Result<(), anyhow::Error> {
|
||||
let client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
|
||||
if client.is_none() {
|
||||
bail!("Client is not yet initialised");
|
||||
}
|
||||
let nym_client = client
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
|
||||
|
||||
let c_str = unsafe {
|
||||
if recipient.is_null() {
|
||||
bail!("recipient is null");
|
||||
}
|
||||
let c_str = CStr::from_ptr(recipient);
|
||||
c_str
|
||||
};
|
||||
let r_str = c_str.to_str().unwrap();
|
||||
let recipient = r_str.parse().unwrap();
|
||||
let c_str = unsafe {
|
||||
if message.is_null() {
|
||||
bail!("message is null");
|
||||
}
|
||||
let c_str = CStr::from_ptr(message);
|
||||
c_str
|
||||
};
|
||||
let message = c_str.to_str().unwrap();
|
||||
|
||||
// send message
|
||||
RUNTIME.block_on(async move {
|
||||
nym_client.send_plain_message(recipient, message).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn reply(recipient: *const c_char, message: *const c_char) -> c_int {
|
||||
match reply_internal(recipient, message) {
|
||||
Ok(_) => StatusCode::NoError as c_int,
|
||||
Err(_) => StatusCode::ReplyError as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
fn reply_internal(
|
||||
recipient: *const c_char,
|
||||
message: *const c_char,
|
||||
) -> anyhow::Result<(), anyhow::Error> {
|
||||
let client = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
|
||||
if client.is_none() {
|
||||
bail!("Client is not yet initialised");
|
||||
}
|
||||
let nym_client = client
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
|
||||
|
||||
let recipient = unsafe {
|
||||
if recipient.is_null() {
|
||||
bail!("recipient is null");
|
||||
}
|
||||
let r_str = CStr::from_ptr(recipient).to_string_lossy().into_owned();
|
||||
AnonymousSenderTag::try_from_base58_string(r_str)
|
||||
.expect("could not construct AnonymousSenderTag from supplied value")
|
||||
};
|
||||
let message = unsafe {
|
||||
if message.is_null() {
|
||||
bail!("message is null");
|
||||
}
|
||||
let c_str = CStr::from_ptr(message);
|
||||
let r_str = c_str.to_str().unwrap();
|
||||
r_str
|
||||
};
|
||||
RUNTIME.block_on(async move {
|
||||
nym_client.send_reply(recipient, message).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn listen_for_incoming(callback: CMessageCallback) -> c_int {
|
||||
match listen_for_incoming_internal(callback) {
|
||||
Ok(_) => StatusCode::NoError as c_int,
|
||||
Err(_) => StatusCode::ListenError as c_int,
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_for_incoming_internal(callback: CMessageCallback) -> anyhow::Result<(), anyhow::Error> {
|
||||
let mut binding = NYM_CLIENT.lock().expect("could not lock NYM_CLIENT");
|
||||
if binding.is_none() {
|
||||
bail!("recipient is null");
|
||||
}
|
||||
let client = binding
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("could not get client as_ref()"))?;
|
||||
|
||||
RUNTIME.block_on(async move {
|
||||
let received = wait_for_non_empty_message(client).await?;
|
||||
let message_ptr = received.message.as_ptr();
|
||||
let message_length = received.message.len();
|
||||
let c_string = CString::new(received.sender_tag.unwrap().to_string())?;
|
||||
let sender_ptr = c_string.as_ptr();
|
||||
// stop deallocation when out of scope as passing raw ptr to it elsewhere
|
||||
forget(received);
|
||||
let rec_for_c = ReceivedMessage {
|
||||
message: message_ptr,
|
||||
size: message_length,
|
||||
sender_tag: sender_ptr,
|
||||
};
|
||||
callback(rec_for_c);
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait_for_non_empty_message(
|
||||
client: &mut MixnetClient,
|
||||
) -> anyhow::Result<ReconstructedMessage> {
|
||||
while let Some(mut new_message) = client.wait_for_messages().await {
|
||||
if !new_message.is_empty() {
|
||||
return new_message
|
||||
.pop()
|
||||
.ok_or_else(|| anyhow!("could not get client as_ref()"));
|
||||
}
|
||||
}
|
||||
bail!("(Rust) did not receive any non-empty message")
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <boost/chrono.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
//Rust function & type signatures
|
||||
extern "C" {
|
||||
struct ReceivedMessage {
|
||||
const uint8_t* message;
|
||||
size_t size;
|
||||
const char* sender_tag;
|
||||
};
|
||||
|
||||
void* init_logging();
|
||||
char init_ephemeral();
|
||||
char get_self_address(void (*callback)(const char*));
|
||||
char send_message(const char*, const char*);
|
||||
char listen_for_incoming(void (*callback)(ReceivedMessage));
|
||||
char reply(const char*, const char*);
|
||||
}
|
||||
|
||||
//return code for error handling
|
||||
char return_code = 0;
|
||||
// bytes for sender tag
|
||||
char sender_tag[22];
|
||||
// nym address
|
||||
char addr[134];
|
||||
// test messages
|
||||
char message[14] = "Hello World";
|
||||
char reply_message[14] = "Reply World";
|
||||
|
||||
void string_callback_function(const char* c_string) {
|
||||
std::cout << "(c++) callback received: " << c_string << std::endl;
|
||||
strcpy(addr, c_string);
|
||||
}
|
||||
|
||||
void incoming_message_callback(ReceivedMessage received) {
|
||||
// this is where you deal with the incoming message -
|
||||
// in this case we'll just log it and save sender_tag to a pre-allocated
|
||||
// buffer to reply to the message further down in main()
|
||||
std::cout << "(c++) sender tag: " << received.sender_tag << std::endl;
|
||||
std::cout << "(c++) message: " << received.message << std::endl;
|
||||
std::cout << "(c++) message length : " << received.size << std::endl;
|
||||
const char* incoming_sender_tag = received.sender_tag;
|
||||
strcpy(sender_tag, incoming_sender_tag);
|
||||
}
|
||||
|
||||
// an overly simplified example - handle the error however you wish
|
||||
int handle(char return_code) {
|
||||
if (return_code == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// initialise Nym client logging - this is quite verbose but very informative
|
||||
init_logging();
|
||||
|
||||
// blocking thread example with error return code:
|
||||
// - package fn
|
||||
// - obtain as future
|
||||
// - execute
|
||||
// - get() returned val
|
||||
// - handle val
|
||||
boost::packaged_task<char> init(boost::bind(init_ephemeral));
|
||||
boost::unique_future<char> init_future = init.get_future();
|
||||
init();
|
||||
return_code = init_future.get();
|
||||
handle(return_code);
|
||||
|
||||
// get_self_addr is sync so no thread required: this is the only exposed rust fn that isn't async
|
||||
return_code = get_self_address(string_callback_function);
|
||||
handle(return_code);
|
||||
|
||||
// send a message through the mixnet - in this case to ourselves
|
||||
std::cout << "(c++) message to send through mixnet: " << message << std::endl;
|
||||
boost::packaged_task<char> send(boost::bind(send_message, addr, message));
|
||||
boost::unique_future<char> send_future = send.get_future();
|
||||
send();
|
||||
return_code = send_future.get();
|
||||
handle(return_code);
|
||||
|
||||
// listen out for incoming messages: in the future the client can be split into a listening and a sending client,
|
||||
// allowing for this to run as a persistent process in its own thread and not have to block but instead be running
|
||||
// concurrently
|
||||
boost::packaged_task<char> listen(boost::bind(listen_for_incoming, incoming_message_callback));
|
||||
boost::unique_future<char> listen_future = listen.get_future();
|
||||
listen();
|
||||
return_code = listen_future.get();
|
||||
handle(return_code);
|
||||
|
||||
// replying to incoming message (from ourselves) with SURBs- note that sending a message to a recipient and
|
||||
// replying to an incoming are different functions
|
||||
boost::packaged_task<char> reply_fn(boost::bind(reply, sender_tag, reply_message));
|
||||
boost::unique_future<char> reply_future = reply_fn.get_future();
|
||||
reply_fn();
|
||||
return_code = reply_future.get();
|
||||
handle(return_code);
|
||||
|
||||
// sleep so that the nym side logging can catch up - in reality you'd have another process running to keep logging
|
||||
// going, so this is only necessary for this reference implementation
|
||||
std::this_thread::sleep_for(std::chrono::seconds(40));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user