diff --git a/Cargo.lock b/Cargo.lock index 40fed3c..6015cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "clientcom_c" +version = "0.1.0" +dependencies = [ + "clientcom", + "lazy_static", + "tokio", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -603,6 +612,12 @@ dependencies = [ "signature", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.172" diff --git a/Cargo.toml b/Cargo.toml index 2c3c83f..21815d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,10 @@ [workspace] -members = ["connserver", "enc", "net", "clientcom"] +members = ["connserver", "enc", "net", "clientcom", "clientcom-c"] resolver = "2" + +[profile.release-with-debug] +opt-level = 3 +debug = true +lto = true +panic = "abort" +inherits = "release" diff --git a/clientcom-c/Cargo.toml b/clientcom-c/Cargo.toml new file mode 100644 index 0000000..dba40ce --- /dev/null +++ b/clientcom-c/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clientcom_c" +version = "0.1.0" +edition = "2024" + +[dependencies] +clientcom = { path = "../clientcom" } +lazy_static = "1.5.0" +tokio = { version = "1.44.2", features = ["macros"] } + +[lib] +crate-type = ["cdylib", "staticlib"] diff --git a/clientcom-c/README.md b/clientcom-c/README.md new file mode 100644 index 0000000..0f060f8 --- /dev/null +++ b/clientcom-c/README.md @@ -0,0 +1,5 @@ +# Voltlane `clientcom` C bindings + +Library: `libclientcom_c.so` or `libclientcom_c.a` + +Header: [`include/voltlane/clientcom.h`](./include/voltlane/clientcom) diff --git a/clientcom-c/include/voltlane/clientcom.h b/clientcom-c/include/voltlane/clientcom.h new file mode 100644 index 0000000..cf06af0 --- /dev/null +++ b/clientcom-c/include/voltlane/clientcom.h @@ -0,0 +1,70 @@ +#ifndef VOLTLANE_CLIENTCOM_H +#define VOLTLANE_CLIENTCOM_H + +//! This file is the hand-written header file for the clientcom C bindings. +//! This is hand-written to ensure clean code and to avoid unnecessary steps +//! and include documentation, so people don't ever have to look at Rust if +//! they don't want to. +//! +//! NOTE: This also means that this API varies WILDLY from the Rust api, simply +//! because it's unnecessary to introduce the same abstractions in C as in Rust. +//! This, however, doesn't mean that this library loses out on any of the +//! features. +//! +//! NOTE 2: NOTHING here is threadsafe. If you want to use this in a multithreaded +//! application, you need to use your own mutexes. It's plenty if you lock every +//! vl_* call with a mutex, and ensure that you always copy out received messages +//! before unlocking. + +#include + +// Opaque type, there's nothing to see or do here. +typedef void vl_connection; + +typedef struct { + // The message data. This is NOT OWNING, please don't try to free it. + // This memory is invalidated by the next call to vl_connection_receive. + char* data; + // The size of the data in bytes. + // Why is this not size_t? Because https://github.com/rust-lang/rust/issues/88345 + unsigned long long size; +} vl_message; + +// Creates a new voltlane connection to the given address. +// +// Returns NULL on failure. +vl_connection* vl_connection_new(const char* address); + +// Closes the connection and frees the memory. +void vl_connection_free(vl_connection* conn); + +// Receives a message from the server. +// +// The returned memory is managed, and does not need to be freed (doing so +// is erroneous). The memory is reused for the next message, so if you want to +// keep the message for longer, you need to copy it. +// Returns a message with a NULL data pointer on failure, otherwise blocks until +// a message is received and returns it. +vl_message vl_connection_recv(vl_connection* conn); + +// Sends a message to the server. +// +// The message is copied, so you don't need to worry about the memory being +// invalidated. +// Returns 0 on success, -1 on failure. +int vl_connection_send(vl_connection* conn, const char* message, size_t size); + +// Returns the last error message. +// +// This is a static buffer, so you don't need to free it. +const char* vl_get_last_error(void); + +// Attempts to reconnect to the server. +// +// Call this ONLY if _recv or _send has failed, or you *know* the +// connection is gone. You can try to send on the connection to see +// if it's still okay, but either way you MUST make sure that the +// connection has failed before calling this. +int vl_connection_reconnect(vl_connection* conn); + +#endif // VOLTLANE_CLIENTCOM_H diff --git a/clientcom-c/src/lib.rs b/clientcom-c/src/lib.rs new file mode 100644 index 0000000..c94134a --- /dev/null +++ b/clientcom-c/src/lib.rs @@ -0,0 +1,147 @@ +use lazy_static::lazy_static; +use std::ffi::c_int; +use std::ffi::{self, c_char, c_ulonglong, c_void}; + +use clientcom::Connection; +use clientcom::net; + +pub static mut LAST_ERROR: *const c_char = std::ptr::null(); + +lazy_static! { + static ref tokio_rt: tokio::runtime::Runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); +} + +#[repr(C)] +pub struct VlMessage { + data: *const c_char, + size: c_ulonglong, +} + +fn save_error(context: impl std::fmt::Display, err: impl std::fmt::Display) { + unsafe { + LAST_ERROR = ffi::CString::new(format!("{}: {}", context, err)) + .unwrap() + .into_raw(); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_get_last_error() -> *const c_char { + unsafe { LAST_ERROR } +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_connection_new(address: *const c_char) -> *const c_void { + let address = unsafe { ffi::CStr::from_ptr(address) }; + let address = match address.to_str() { + Ok(val) => val, + Err(err) => { + save_error("vl_connection_new: address is invalid", err); + return std::ptr::null(); + } + }; + let _guard = tokio_rt.enter(); + let conn = tokio_rt.block_on(Connection::new(address)); + match conn { + Ok(conn) => { + let conn = Box::new(conn); + let conn = Box::into_raw(conn); + return conn as *const c_void; + } + Err(err) => { + save_error("vl_connection_new: creating connection failed", err); + return std::ptr::null(); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_connection_free(conn: *const c_void) { + if conn.is_null() { + return; + } + let conn = unsafe { Box::from_raw(conn as *mut Connection) }; + drop(conn); +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_connection_send( + conn: *const c_void, + message: *const c_char, + size: c_ulonglong, +) -> c_int { + if conn.is_null() { + save_error("vl_connection_send: conn is null", "conn is null"); + return -1; + } + let conn = unsafe { &mut *(conn as *mut Connection) }; + let message = unsafe { std::slice::from_raw_parts(message as *const u8, size as usize) }; + let _guard = tokio_rt.enter(); + let result = tokio_rt.block_on(net::send_size_prefixed(&mut conn.write, message)); + if result.is_err() { + save_error( + "vl_connection_send: sending message failed", + result.err().unwrap(), + ); + return -1; + } + 0 +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_connection_recv(conn: *const c_void) -> VlMessage { + if conn.is_null() { + save_error("vl_connection_recv: conn is null", "conn is null"); + return VlMessage { + data: std::ptr::null(), + size: 0, + }; + } + let conn = unsafe { &mut *(conn as *mut Connection) }; + + let result = tokio_rt.block_on(net::recv_size_prefixed(&mut conn.read)); + match result { + Ok(buffer) => { + let message = VlMessage { + data: buffer.as_ptr() as *const c_char, + size: buffer.len() as c_ulonglong, + }; + // NOTE(lion): we dont need to mem::forget or anything, since the buffer is + // BytesMut, which is a reference to the internal buffer. The C API we expose + // documents that this buffer is only valid until the next call to vl_connection_recv, + // which is accurate. + return message; + } + Err(err) => { + save_error("vl_connection_recv: receiving message failed", err); + return VlMessage { + data: std::ptr::null(), + size: 0, + }; + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn vl_connection_reconnect( + conn: *const c_void, +) -> c_int { + if conn.is_null() { + save_error("vl_connection_reconnect: conn is null", "conn is null"); + return -1; + } + let conn = unsafe { &mut *(conn as *mut Connection) }; + let _guard = tokio_rt.enter(); + let result = tokio_rt.block_on(conn.reconnect()); + if result.is_err() { + save_error( + "vl_connection_reconnect: reconnecting failed", + result.err().unwrap(), + ); + return -1; + } + 0 +} diff --git a/examples/test-client-c/.gitignore b/examples/test-client-c/.gitignore new file mode 100644 index 0000000..9151984 --- /dev/null +++ b/examples/test-client-c/.gitignore @@ -0,0 +1,3 @@ +bin/ +.cache/ +compile_commands.json diff --git a/examples/test-client-c/Makefile b/examples/test-client-c/Makefile new file mode 100644 index 0000000..2f11e81 --- /dev/null +++ b/examples/test-client-c/Makefile @@ -0,0 +1,17 @@ + +all: bin/test-client-c + +.PHONY: all clean + +bin/test-client-c: main.c bin/libclientcom_c.a + ${CC} -o $@ $< -Lbin -lclientcom_c -I../../clientcom-c/include -lm -g -O3 -flto + +bin/libclientcom_c.a: ../../target/release/libclientcom_c.a + mkdir -p bin + cp $< $@ + +bin: + mkdir -p bin + +clean: + rm -rf bin diff --git a/examples/test-client-c/main.c b/examples/test-client-c/main.c new file mode 100644 index 0000000..4e9b889 --- /dev/null +++ b/examples/test-client-c/main.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include + +// The following example opens a new connection to the connserver +// and subsequently enters a REPL mode; you type a message, and it's +// sent via the voltlane protocol to the master server, which is connected +// to the connserver. If a connection error occurs, a reconnection is +// attempted using the voltlane protocol's asymmetric key authentication. + +int main(void) { + vl_connection* conn = vl_connection_new("127.0.0.1:42000"); + int exit_code = 0; + if (!conn) { + fprintf(stderr, "%s\n", vl_get_last_error()); + exit_code = 1; + goto cleanup; + } + + char buf[1024]; + memset(buf, 0, sizeof(buf)); + + int tty = isatty(STDIN_FILENO); + + while (1) { + buf[sizeof(buf) - 1] = 0; + int n = 0; + char* res = NULL; + if (tty) { + char* r = fgets(buf, sizeof(buf) - 1, stdin); + if (!r) { + break; + } + res = r; + n = strlen(res); + } else { + int read = fread(buf, 1, sizeof(buf), stdin); + if (read == 0) { + break; + } + res = buf; + n = read; + } + int rc = vl_connection_send(conn, buf, n); + if (rc < 0) { + fprintf(stderr, "%s\n", vl_get_last_error()); + if (vl_connection_reconnect(conn) < 0) { + exit_code = 1; + break; + } else { + fprintf(stderr, "reconnected!\n"); + continue; + } + } + + vl_message msg = vl_connection_recv(conn); + if (!msg.data) { + fprintf(stderr, "%s\n", vl_get_last_error()); + if (vl_connection_reconnect(conn) < 0) { + exit_code = 1; + break; + } else { + fprintf(stderr, "reconnected!\n"); + continue; + } + } + fwrite(msg.data, 1, msg.size, stdout); + } + +cleanup: + vl_connection_free(conn); + return exit_code; +}