* 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:
mx
2024-01-24 15:05:59 +00:00
committed by GitHub
parent dbe6a5de7d
commit ab4cc9b282
8 changed files with 6200 additions and 1 deletions
+1 -1
View File
@@ -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"]
+2
View File
@@ -0,0 +1,2 @@
src/main
target/
+5728
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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"
+46
View File
@@ -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.
+51
View File
@@ -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
+238
View File
@@ -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")
}
+111
View File
@@ -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;
}