From 2e7f0c7433f7fcc5b2781b29b92a19e88db49aba Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 1 May 2025 23:17:03 +0200 Subject: [PATCH 1/5] feat: add C bindings and example --- Cargo.lock | 15 +++ Cargo.toml | 2 +- clientcom-c/Cargo.toml | 12 +++ clientcom-c/README.md | 5 + clientcom-c/include/voltlane/clientcom.h | 58 ++++++++++ clientcom-c/src/lib.rs | 128 +++++++++++++++++++++++ examples/test-client-c/.gitignore | 3 + examples/test-client-c/Makefile | 17 +++ examples/test-client-c/main.c | 43 ++++++++ 9 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 clientcom-c/Cargo.toml create mode 100644 clientcom-c/README.md create mode 100644 clientcom-c/include/voltlane/clientcom.h create mode 100644 clientcom-c/src/lib.rs create mode 100644 examples/test-client-c/.gitignore create mode 100644 examples/test-client-c/Makefile create mode 100644 examples/test-client-c/main.c 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..8b9f894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["connserver", "enc", "net", "clientcom"] +members = ["connserver", "enc", "net", "clientcom", "clientcom-c"] resolver = "2" 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..44ccbac --- /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/clientcom) diff --git a/clientcom-c/include/voltlane/clientcom.h b/clientcom-c/include/voltlane/clientcom.h new file mode 100644 index 0000000..c41bfe1 --- /dev/null +++ b/clientcom-c/include/voltlane/clientcom.h @@ -0,0 +1,58 @@ +#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); + +#endif // VOLTLANE_CLIENTCOM_H diff --git a/clientcom-c/src/lib.rs b/clientcom-c/src/lib.rs new file mode 100644 index 0000000..1d1a1fb --- /dev/null +++ b/clientcom-c/src/lib.rs @@ -0,0 +1,128 @@ +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) { + eprintln!("VOLTLANE: {}", format!("{}: {}", context, err)); + 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 _guard = tokio_rt.enter(); + 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, + }; + } + } +} 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..8a44a23 --- /dev/null +++ b/examples/test-client-c/main.c @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +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)); + + while (1) { + buf[sizeof(buf) - 1] = 0; + char* res = fgets(buf, sizeof(buf) - 1, stdin); + if (!res) { + break; + } + int rc = vl_connection_send(conn, res, strlen(res)); + if (rc < 0) { + fprintf(stderr, "%s\n", vl_get_last_error()); + exit_code = 1; + break; + } + + vl_message msg = vl_connection_recv(conn); + if (!msg.data) { + fprintf(stderr, "%s\n", vl_get_last_error()); + exit_code = 1; + break; + } + printf("%.*s", (int)msg.size, msg.data); + } + +cleanup: + vl_connection_free(conn); + return exit_code; +} From c083e874d6a012054aed3fc29e8248fb25737272 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 1 May 2025 23:19:25 +0200 Subject: [PATCH 2/5] docs: fix clientcom.h link --- clientcom-c/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clientcom-c/README.md b/clientcom-c/README.md index 44ccbac..0f060f8 100644 --- a/clientcom-c/README.md +++ b/clientcom-c/README.md @@ -2,4 +2,4 @@ Library: `libclientcom_c.so` or `libclientcom_c.a` -Header: [`include/voltlane/clientcom.h`](./include/clientcom) +Header: [`include/voltlane/clientcom.h`](./include/voltlane/clientcom) From ff61b6367bdeda88d5c2f67d5d9ef405985f7385 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 1 May 2025 23:52:01 +0200 Subject: [PATCH 3/5] feat: add reconnection to C bindings --- clientcom-c/include/voltlane/clientcom.h | 12 ++++++++++ clientcom-c/src/lib.rs | 21 +++++++++++++++++ examples/test-client-c/main.c | 29 ++++++++++++++++++------ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/clientcom-c/include/voltlane/clientcom.h b/clientcom-c/include/voltlane/clientcom.h index c41bfe1..cf06af0 100644 --- a/clientcom-c/include/voltlane/clientcom.h +++ b/clientcom-c/include/voltlane/clientcom.h @@ -31,6 +31,7 @@ typedef struct { } vl_message; // Creates a new voltlane connection to the given address. +// // Returns NULL on failure. vl_connection* vl_connection_new(const char* address); @@ -38,6 +39,7 @@ vl_connection* vl_connection_new(const char* address); 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. @@ -46,13 +48,23 @@ void vl_connection_free(vl_connection* conn); 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 index 1d1a1fb..4709e8f 100644 --- a/clientcom-c/src/lib.rs +++ b/clientcom-c/src/lib.rs @@ -126,3 +126,24 @@ pub extern "C" fn vl_connection_recv(conn: *const c_void) -> VlMessage { } } } + +#[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/main.c b/examples/test-client-c/main.c index 8a44a23..bbeb7f8 100644 --- a/examples/test-client-c/main.c +++ b/examples/test-client-c/main.c @@ -1,8 +1,13 @@ -#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; @@ -23,16 +28,26 @@ int main(void) { } int rc = vl_connection_send(conn, res, strlen(res)); if (rc < 0) { - fprintf(stderr, "%s\n", vl_get_last_error()); - exit_code = 1; - break; + fprintf(stderr, "lost connection: %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()); - exit_code = 1; - break; + fprintf(stderr, "lost connection: %s\n", vl_get_last_error()); + if (vl_connection_reconnect(conn) < 0) { + exit_code = 1; + break; + } else { + fprintf(stderr, "reconnected!\n"); + continue; + } } printf("%.*s", (int)msg.size, msg.data); } From 22ad04523ab8c68dfca6521fc4046eaf195f68e1 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 1 May 2025 23:52:49 +0200 Subject: [PATCH 4/5] chore: remove debug print --- clientcom-c/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/clientcom-c/src/lib.rs b/clientcom-c/src/lib.rs index 4709e8f..c195002 100644 --- a/clientcom-c/src/lib.rs +++ b/clientcom-c/src/lib.rs @@ -21,7 +21,6 @@ pub struct VlMessage { } fn save_error(context: impl std::fmt::Display, err: impl std::fmt::Display) { - eprintln!("VOLTLANE: {}", format!("{}: {}", context, err)); unsafe { LAST_ERROR = ffi::CString::new(format!("{}: {}", context, err)) .unwrap() From bdafb10cc4fd6538de954a650a85aa3e0991f752 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Sat, 3 May 2025 00:16:52 +0200 Subject: [PATCH 5/5] fix: improve performance of test-client-c it's now on par again with test-client2 man i should rename those huh --- Cargo.toml | 7 +++++++ clientcom-c/src/lib.rs | 1 - examples/test-client-c/main.c | 30 +++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8b9f894..21815d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,10 @@ [workspace] 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/src/lib.rs b/clientcom-c/src/lib.rs index c195002..c94134a 100644 --- a/clientcom-c/src/lib.rs +++ b/clientcom-c/src/lib.rs @@ -102,7 +102,6 @@ pub extern "C" fn vl_connection_recv(conn: *const c_void) -> VlMessage { } let conn = unsafe { &mut *(conn as *mut Connection) }; - let _guard = tokio_rt.enter(); let result = tokio_rt.block_on(net::recv_size_prefixed(&mut conn.read)); match result { Ok(buffer) => { diff --git a/examples/test-client-c/main.c b/examples/test-client-c/main.c index bbeb7f8..4e9b889 100644 --- a/examples/test-client-c/main.c +++ b/examples/test-client-c/main.c @@ -1,5 +1,6 @@ #include #include +#include #include // The following example opens a new connection to the connserver @@ -20,15 +21,30 @@ int main(void) { char buf[1024]; memset(buf, 0, sizeof(buf)); + int tty = isatty(STDIN_FILENO); + while (1) { buf[sizeof(buf) - 1] = 0; - char* res = fgets(buf, sizeof(buf) - 1, stdin); - if (!res) { - break; + 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, res, strlen(res)); + int rc = vl_connection_send(conn, buf, n); if (rc < 0) { - fprintf(stderr, "lost connection: %s\n", vl_get_last_error()); + fprintf(stderr, "%s\n", vl_get_last_error()); if (vl_connection_reconnect(conn) < 0) { exit_code = 1; break; @@ -40,7 +56,7 @@ int main(void) { vl_message msg = vl_connection_recv(conn); if (!msg.data) { - fprintf(stderr, "lost connection: %s\n", vl_get_last_error()); + fprintf(stderr, "%s\n", vl_get_last_error()); if (vl_connection_reconnect(conn) < 0) { exit_code = 1; break; @@ -49,7 +65,7 @@ int main(void) { continue; } } - printf("%.*s", (int)msg.size, msg.data); + fwrite(msg.data, 1, msg.size, stdout); } cleanup: