Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions clientcom-c/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
5 changes: 5 additions & 0 deletions clientcom-c/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Voltlane `clientcom` C bindings

Library: `libclientcom_c.so` or `libclientcom_c.a`

Header: [`include/voltlane/clientcom.h`](./include/voltlane/clientcom)
70 changes: 70 additions & 0 deletions clientcom-c/include/voltlane/clientcom.h
Original file line number Diff line number Diff line change
@@ -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 <stddef.h>

// 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
147 changes: 147 additions & 0 deletions clientcom-c/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions examples/test-client-c/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
.cache/
compile_commands.json
17 changes: 17 additions & 0 deletions examples/test-client-c/Makefile
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions examples/test-client-c/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <voltlane/clientcom.h>

// 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;
}