feat(ffi): add async data plane API (#2321)

* feat(ffi): add async data plane API
* feat(ffi): add async data plane examples
* test(ffi): make async Go dataplane tests self-contained
* docs(ffi): document Go async dataplane API
* docs(android): document dataplane JNI API
This commit is contained in:
KKRainbow
2026-06-06 21:52:50 +08:00
committed by GitHub
parent 4a25ca934b
commit 793b57c2a1
18 changed files with 5219 additions and 74 deletions
@@ -465,7 +465,7 @@ pub(crate) unsafe fn start_config_server_client(
clear_last_callback_error();
#[cfg(feature = "ffi-dataplane")]
let _data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
let data_plane_usage_guard = match crate::data_plane::lock_for_config_server_start() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&err);
@@ -474,6 +474,9 @@ pub(crate) unsafe fn start_config_server_client(
};
CONFIG_SERVER_CLIENT_ACTIVE.store(true, Ordering::Release);
#[cfg(feature = "ffi-dataplane")]
drop(data_plane_usage_guard);
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
let client = match ASYNC_RUNTIME.block_on(run_web_client(
&config_server_url,
+125 -51
View File
@@ -37,22 +37,22 @@ static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
once_cell::sync::Lazy::new(|| RwLock::new(()));
#[cfg(feature = "ffi-dataplane")]
struct DataPlaneHandle {
instance_id: uuid::Uuid,
runtime: tokio::runtime::Handle,
pub(crate) struct DataPlaneHandle {
pub(crate) instance_id: uuid::Uuid,
pub(crate) runtime: tokio::runtime::Handle,
// Cancelled by close() to wake any in-flight op on this handle.
close_token: CancellationToken,
resource: DataPlaneResource,
pub(crate) close_token: CancellationToken,
pub(crate) resource: DataPlaneResource,
}
#[cfg(feature = "ffi-dataplane")]
struct TcpHalves {
read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
pub(crate) struct TcpHalves {
pub(crate) read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
pub(crate) write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
}
#[cfg(feature = "ffi-dataplane")]
enum DataPlaneResource {
pub(crate) enum DataPlaneResource {
Tcp(Arc<TcpHalves>),
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
Udp(Arc<DataPlaneUdpSocket>),
@@ -61,17 +61,17 @@ enum DataPlaneResource {
// Several helper functions for FFI data plane operations to facilitate logic reuse.
#[cfg(feature = "ffi-dataplane")]
fn next_handle() -> u64 {
pub(crate) fn next_handle() -> u64 {
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
}
#[cfg(feature = "ffi-dataplane")]
fn timeout_duration(timeout_ms: u64) -> Duration {
pub(crate) fn timeout_duration(timeout_ms: u64) -> Duration {
Duration::from_millis(timeout_ms)
}
#[cfg(feature = "ffi-dataplane")]
unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
pub(crate) unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
if ptr.is_null() {
set_error_msg(&format!("{} is null", name));
return None;
@@ -84,12 +84,12 @@ unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<Str
}
#[cfg(feature = "ffi-dataplane")]
fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
pub(crate) fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
}
#[cfg(feature = "ffi-dataplane")]
fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
pub(crate) fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
let ip = match host.parse::<IpAddr>() {
Ok(ip) => ip,
Err(e) => {
@@ -103,7 +103,7 @@ fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
/// Encode an IP address for FFI return. Returns `*mut c_char` to match
/// `CString::into_raw`; caller releases it via `free_string`.
#[cfg(feature = "ffi-dataplane")]
fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
pub(crate) fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
match std::ffi::CString::new(ip.to_string()) {
Ok(s) => Some(s.into_raw()),
Err(e) => {
@@ -114,7 +114,7 @@ fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
}
#[cfg(feature = "ffi-dataplane")]
fn get_runtime_handle(
pub(crate) fn get_runtime_handle(
inst_id: &uuid::Uuid,
deadline: std::time::Instant,
) -> Option<tokio::runtime::Handle> {
@@ -127,7 +127,7 @@ fn get_runtime_handle(
}
#[cfg(feature = "ffi-dataplane")]
fn insert_tcp_stream_handle(
pub(crate) fn insert_tcp_stream_handle(
instance_id: uuid::Uuid,
runtime: tokio::runtime::Handle,
stream: DataPlaneTcpStream,
@@ -150,17 +150,71 @@ fn insert_tcp_stream_handle(
}
#[cfg(feature = "ffi-dataplane")]
fn get_tcp_stream(
pub(crate) fn insert_tcp_listener_handle(
instance_id: uuid::Uuid,
runtime: tokio::runtime::Handle,
listener: DataPlaneTcpListener,
) -> u64 {
let handle = next_handle();
DATA_PLANE_HANDLES.insert(
handle,
DataPlaneHandle {
instance_id,
runtime,
close_token: CancellationToken::new(),
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(listener))),
},
);
handle
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn insert_udp_socket_handle(
instance_id: uuid::Uuid,
runtime: tokio::runtime::Handle,
socket: DataPlaneUdpSocket,
) -> u64 {
let handle = next_handle();
DATA_PLANE_HANDLES.insert(
handle,
DataPlaneHandle {
instance_id,
runtime,
close_token: CancellationToken::new(),
resource: DataPlaneResource::Udp(Arc::new(socket)),
},
);
handle
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_tcp_stream(
handle: u64,
) -> Option<(Arc<TcpHalves>, tokio::runtime::Handle, CancellationToken)> {
get_tcp_stream_with_instance(handle)
.map(|(halves, runtime, close_token, _)| (halves, runtime, close_token))
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_tcp_stream_with_instance(
handle: u64,
) -> Option<(
Arc<TcpHalves>,
tokio::runtime::Handle,
CancellationToken,
uuid::Uuid,
)> {
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
set_error_msg("tcp stream handle not found");
return None;
};
match &h.resource {
DataPlaneResource::Tcp(halves) => {
Some((halves.clone(), h.runtime.clone(), h.close_token.clone()))
}
DataPlaneResource::Tcp(halves) => Some((
halves.clone(),
h.runtime.clone(),
h.close_token.clone(),
h.instance_id,
)),
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
set_error_msg("handle is not a tcp stream");
None
@@ -169,7 +223,7 @@ fn get_tcp_stream(
}
#[cfg(feature = "ffi-dataplane")]
fn get_tcp_listener(
pub(crate) fn get_tcp_listener(
handle: u64,
) -> Option<(
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
@@ -196,21 +250,37 @@ fn get_tcp_listener(
}
#[cfg(feature = "ffi-dataplane")]
fn get_udp_socket(
pub(crate) fn get_udp_socket(
handle: u64,
) -> Option<(
Arc<DataPlaneUdpSocket>,
tokio::runtime::Handle,
CancellationToken,
)> {
get_udp_socket_with_instance(handle)
.map(|(socket, runtime, close_token, _)| (socket, runtime, close_token))
}
#[cfg(feature = "ffi-dataplane")]
pub(crate) fn get_udp_socket_with_instance(
handle: u64,
) -> Option<(
Arc<DataPlaneUdpSocket>,
tokio::runtime::Handle,
CancellationToken,
uuid::Uuid,
)> {
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
set_error_msg("udp socket handle not found");
return None;
};
match &h.resource {
DataPlaneResource::Udp(socket) => {
Some((socket.clone(), h.runtime.clone(), h.close_token.clone()))
}
DataPlaneResource::Udp(socket) => Some((
socket.clone(),
h.runtime.clone(),
h.close_token.clone(),
h.instance_id,
)),
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
set_error_msg("handle is not a udp socket");
None
@@ -224,6 +294,10 @@ pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
return;
}
let _data_plane_usage_guard = DATA_PLANE_USAGE_LOCK
.write()
.unwrap_or_else(|err| err.into_inner());
DATA_PLANE_HANDLES.retain(|_, handle| {
if ids.contains(&handle.instance_id) {
handle.close_token.cancel();
@@ -232,13 +306,14 @@ pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
true
}
});
crate::data_plane_async::remove_ops_by_instance_ids(ids);
}
#[cfg(not(feature = "ffi-dataplane"))]
pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {}
#[cfg(feature = "ffi-dataplane")]
fn data_plane_rejected() -> bool {
pub(crate) fn data_plane_rejected() -> bool {
if in_config_server_callback() {
set_error_msg("cannot use data plane from config server callback");
true
@@ -251,7 +326,7 @@ fn data_plane_rejected() -> bool {
}
#[cfg(feature = "ffi-dataplane")]
fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
pub(crate) fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
if data_plane_rejected() {
return None;
}
@@ -303,7 +378,7 @@ pub(crate) fn lock_for_config_server_start()
let guard = DATA_PLANE_USAGE_LOCK
.write()
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
if !DATA_PLANE_HANDLES.is_empty() {
if !DATA_PLANE_HANDLES.is_empty() || crate::data_plane_async::has_live_ops() {
return Err("cannot start config server client while data plane is in use".to_string());
}
Ok(guard)
@@ -413,18 +488,7 @@ pub(crate) unsafe fn data_plane_tcp_bind(
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
let handle = next_handle();
DATA_PLANE_HANDLES.insert(
handle,
DataPlaneHandle {
instance_id: inst_id,
runtime,
close_token: CancellationToken::new(),
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(
listener,
))),
},
);
let handle = insert_tcp_listener_handle(inst_id, runtime, listener);
unsafe {
*out_local_ip = local_ip as *const std::ffi::c_char;
*out_local_port = local_addr.port();
@@ -600,6 +664,7 @@ pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
Some(guard) => guard,
None => return -1,
};
crate::data_plane_async::cancel_ops_for_handle(handle);
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
matches!(e.resource, DataPlaneResource::Tcp(_))
}) else {
@@ -629,6 +694,7 @@ pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
Some(guard) => guard,
None => return -1,
};
crate::data_plane_async::cancel_ops_for_handle(handle);
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
matches!(e.resource, DataPlaneResource::TcpListener(_))
}) else {
@@ -685,16 +751,7 @@ pub(crate) unsafe fn data_plane_udp_bind(
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
return 0;
};
let handle = next_handle();
DATA_PLANE_HANDLES.insert(
handle,
DataPlaneHandle {
instance_id: inst_id,
runtime,
close_token: CancellationToken::new(),
resource: DataPlaneResource::Udp(Arc::new(socket)),
},
);
let handle = insert_udp_socket_handle(inst_id, runtime, socket);
unsafe {
*out_local_ip = local_ip as *const std::ffi::c_char;
*out_local_port = local_addr.port();
@@ -818,6 +875,7 @@ pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
Some(guard) => guard,
None => return -1,
};
crate::data_plane_async::cancel_ops_for_handle(handle);
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
matches!(e.resource, DataPlaneResource::Udp(_))
}) else {
@@ -851,4 +909,20 @@ mod tests {
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
waiter.join().unwrap();
}
#[test]
fn instance_cleanup_waits_for_data_plane_operation() {
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
let instance_id = Uuid::new_v4();
let (done_tx, done_rx) = mpsc::channel();
let cleaner = std::thread::spawn(move || {
remove_data_plane_handles_by_instance_ids(&[instance_id]);
done_tx.send(()).unwrap();
});
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
drop(read_guard);
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
cleaner.join().unwrap();
}
}
File diff suppressed because it is too large Load Diff
@@ -112,6 +112,34 @@ pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> s
0
}
unsafe fn parse_instance_names(
inst_names: *const *const c_char,
length: usize,
) -> Option<Vec<String>> {
if length == 0 {
return Some(Vec::new());
}
if inst_names.is_null() {
set_error_msg("inst_names is null");
return None;
}
let names = unsafe { std::slice::from_raw_parts(inst_names, length) };
let mut parsed = Vec::with_capacity(length);
for (index, &name) in names.iter().enumerate() {
if name.is_null() {
set_error_msg(&format!("inst_names[{}] is null", index));
return None;
}
parsed.push(
unsafe { std::ffi::CStr::from_ptr(name) }
.to_string_lossy()
.into_owned(),
);
}
Some(parsed)
}
/// # Safety
/// Retain the network instance
pub(crate) unsafe fn retain_network_instance(
@@ -145,17 +173,8 @@ pub(crate) unsafe fn retain_network_instance(
return 0;
}
let inst_names = unsafe {
assert!(!inst_names.is_null());
std::slice::from_raw_parts(inst_names, length)
.iter()
.map(|&name| {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>()
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
};
let removed_ids = INSTANCE_MANAGER
@@ -180,6 +199,54 @@ pub(crate) unsafe fn retain_network_instance(
0
}
/// # Safety
/// Delete named network instances.
pub(crate) unsafe fn delete_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if in_config_server_callback() {
set_error_msg("cannot delete network instances from config server callback");
return -1;
}
wait_for_config_server_delivery();
let _remote_mutation_guard = lock_remote_instance_mutation();
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
Ok(guard) => guard,
Err(err) => {
set_error_msg(&format!("failed to lock instance mutation: {}", err));
return -1;
}
};
if length == 0 {
return 0;
}
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
return -1;
};
let removed_ids = inst_names
.iter()
.filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id.value()))
.collect::<Vec<_>>();
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
set_error_msg(&format!("failed to delete instances: {}", e));
return -1;
}
remove_config_server_tracked_instance_ids(&removed_ids);
remove_data_plane_handles_by_instance_ids(&removed_ids);
for name in inst_names {
INSTANCE_NAME_ID_MAP.remove(&name);
}
0
}
/// # Safety
/// Collect the network infos
pub(crate) unsafe fn collect_network_infos(
+238
View File
@@ -8,6 +8,7 @@
//! - `parse_config`: validate a TOML network config string.
//! - `run_network_instance`: start one local network instance from TOML.
//! - `retain_network_instance`: keep named instances and stop all others.
//! - `delete_network_instance`: stop named local network instances.
//! - `collect_network_infos`: collect running instance info as key/value pairs.
//! - `set_tun_fd`: attach a TUN file descriptor to a named instance.
//!
@@ -28,6 +29,8 @@
//! - `data_plane_udp_send_to`: send one UDP data-plane datagram.
//! - `data_plane_udp_recv_from`: receive one UDP data-plane datagram.
//! - `data_plane_udp_close`: close a UDP data-plane socket.
//! - `data_plane_*_start` / `data_plane_*_finish`: asynchronous data-plane operations.
//! - `data_plane_async_op_*`: poll, wait, cancel, and free asynchronous operations.
//!
//! Shared FFI helper APIs:
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
@@ -35,6 +38,8 @@
mod config_server;
mod data_plane;
#[cfg(feature = "ffi-dataplane")]
mod data_plane_async;
mod error;
mod instance_api;
mod state;
@@ -111,6 +116,30 @@ pub unsafe extern "C" fn retain_network_instance(
unsafe { instance_api::retain_network_instance(inst_names, length) }
}
/// Stop the named network instances.
///
/// Passing `length == 0` is a no-op. When `length > 0`, `inst_names` must point
/// to an array of `length` non-null C strings. Unknown names are ignored.
/// Removed instances are also removed from the FFI name cache and any related
/// data-plane handles are closed.
///
/// This API fails if called from a config-server event callback.
///
/// # Safety
/// If `length > 0`, `inst_names` must be a non-null pointer to an array of
/// `length` non-null pointers to null-terminated UTF-8 strings.
///
/// # Return
/// Returns `0` on success, or `-1` on failure. On failure, call
/// `get_error_msg` on the same thread to retrieve details.
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn delete_network_instance(
inst_names: *const *const c_char,
length: usize,
) -> c_int {
unsafe { instance_api::delete_network_instance(inst_names, length) }
}
/// Collect running network instance information.
///
/// Writes up to `max_length` entries into `infos`. Each returned key is the
@@ -490,6 +519,215 @@ pub extern "C" fn data_plane_udp_close(handle: u64) -> c_int {
data_plane::data_plane_udp_close(handle)
}
// ===== Async Data Plane API =====
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_async_op_status(handle: u64) -> c_int {
data_plane_async::data_plane_async_op_status(handle)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_async_op_wait(handle: u64, timeout_ms: u64) -> c_int {
data_plane_async::data_plane_async_op_wait(handle, timeout_ms)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_async_op_cancel(handle: u64) -> c_int {
data_plane_async::data_plane_async_op_cancel(handle)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_async_op_free(handle: u64) -> c_int {
data_plane_async::data_plane_async_op_free(handle)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_free_bytes(ptr: *const c_uchar, len: u32) {
data_plane_async::data_plane_free_bytes(ptr, len)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_start(
inst_name: *const c_char,
dst_ip: *const c_char,
dst_port: c_ushort,
timeout_ms: u64,
) -> u64 {
unsafe {
data_plane_async::data_plane_tcp_connect_start(inst_name, dst_ip, dst_port, timeout_ms)
}
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_connect_finish(
op_handle: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe {
data_plane_async::data_plane_tcp_connect_finish(op_handle, out_local_ip, out_local_port)
}
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_start(
inst_name: *const c_char,
local_port: c_ushort,
timeout_ms: u64,
) -> u64 {
unsafe { data_plane_async::data_plane_tcp_bind_start(inst_name, local_port, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_bind_finish(
op_handle: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe { data_plane_async::data_plane_tcp_bind_finish(op_handle, out_local_ip, out_local_port) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_start(handle: u64, timeout_ms: u64) -> u64 {
unsafe { data_plane_async::data_plane_tcp_accept_start(handle, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_accept_finish(
op_handle: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
out_peer_ip: *mut *const c_char,
out_peer_port: *mut c_ushort,
) -> u64 {
unsafe {
data_plane_async::data_plane_tcp_accept_finish(
op_handle,
out_local_ip,
out_local_port,
out_peer_ip,
out_peer_port,
)
}
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_start(
handle: u64,
max_len: u32,
timeout_ms: u64,
) -> u64 {
unsafe { data_plane_async::data_plane_tcp_read_start(handle, max_len, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_read_finish(
op_handle: u64,
out_buf: *mut *const c_uchar,
out_len: *mut u32,
) -> c_int {
unsafe { data_plane_async::data_plane_tcp_read_finish(op_handle, out_buf, out_len) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_tcp_write_start(
handle: u64,
buf: *const c_uchar,
len: u32,
timeout_ms: u64,
) -> u64 {
unsafe { data_plane_async::data_plane_tcp_write_start(handle, buf, len, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_tcp_write_finish(op_handle: u64) -> c_int {
data_plane_async::data_plane_tcp_write_finish(op_handle)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_start(
inst_name: *const c_char,
local_port: c_ushort,
timeout_ms: u64,
) -> u64 {
unsafe { data_plane_async::data_plane_udp_bind_start(inst_name, local_port, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_bind_finish(
op_handle: u64,
out_local_ip: *mut *const c_char,
out_local_port: *mut c_ushort,
) -> u64 {
unsafe { data_plane_async::data_plane_udp_bind_finish(op_handle, out_local_ip, out_local_port) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_send_to_start(
handle: u64,
dst_ip: *const c_char,
dst_port: c_ushort,
buf: *const c_uchar,
len: u32,
timeout_ms: u64,
) -> u64 {
unsafe {
data_plane_async::data_plane_udp_send_to_start(
handle, dst_ip, dst_port, buf, len, timeout_ms,
)
}
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub extern "C" fn data_plane_udp_send_to_finish(op_handle: u64) -> c_int {
data_plane_async::data_plane_udp_send_to_finish(op_handle)
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_recv_from_start(
handle: u64,
max_len: u32,
timeout_ms: u64,
) -> u64 {
unsafe { data_plane_async::data_plane_udp_recv_from_start(handle, max_len, timeout_ms) }
}
#[cfg(feature = "ffi-dataplane")]
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
pub unsafe extern "C" fn data_plane_udp_recv_from_finish(
op_handle: u64,
out_buf: *mut *const c_uchar,
out_len: *mut u32,
out_ip: *mut *const c_char,
out_port: *mut c_ushort,
) -> c_int {
unsafe {
data_plane_async::data_plane_udp_recv_from_finish(
op_handle, out_buf, out_len, out_ip, out_port,
)
}
}
// ===== Shared FFI Helper API =====
/// Return the last FFI error message.
@@ -304,6 +304,58 @@ fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
remove_instance_name_ids(&[instance_id]);
}
#[test]
fn delete_network_instance_removes_only_named_instances() {
let keep_id = Uuid::new_v4();
let delete_id = Uuid::new_v4();
let keep_name = format!("keep-{}", keep_id);
let delete_name = format!("delete-{}", delete_id);
for (id, name) in [
(keep_id, keep_name.clone()),
(delete_id, delete_name.clone()),
] {
let cfg = TomlConfigLoader::default();
cfg.set_id(id);
cfg.set_inst_name(name.clone());
INSTANCE_MANAGER
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
.unwrap();
INSTANCE_NAME_ID_MAP.insert(name, id);
}
let delete_name = CString::new(delete_name.clone()).unwrap();
let inst_names = [delete_name.as_ptr()];
assert_eq!(
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
0
);
assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id));
assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none());
INSTANCE_MANAGER
.delete_network_instance(vec![keep_id])
.unwrap();
remove_instance_name_ids(&[keep_id]);
}
#[test]
fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 1) }, -1);
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 1) }, -1);
let inst_names = [std::ptr::null()];
assert_eq!(
unsafe { retain_network_instance(inst_names.as_ptr(), inst_names.len()) },
-1
);
assert_eq!(
unsafe { delete_network_instance(inst_names.as_ptr(), inst_names.len()) },
-1
);
}
#[test]
fn ffi_remote_mutation_lock_uses_manager_lock() {
let manager_guard = INSTANCE_MANAGER
@@ -350,6 +402,7 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
let cfg = CString::new("inst_name = \"callback-test\"\nlisteners = []").unwrap();
assert_eq!(unsafe { run_network_instance(cfg.as_ptr()) }, -1);
assert_eq!(unsafe { retain_network_instance(std::ptr::null(), 0) }, -1);
assert_eq!(unsafe { delete_network_instance(std::ptr::null(), 0) }, -1);
let url = CString::new("ring://test/token").unwrap();
let machine_id = CString::new("test-machine").unwrap();
assert_eq!(
@@ -447,6 +500,34 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
-1
);
assert_eq!(data_plane_udp_close(0), -1);
assert_eq!(data_plane_async_op_status(0), -2);
assert_eq!(data_plane_async_op_wait(0, 0), -2);
assert_eq!(data_plane_async_op_cancel(0), -2);
assert_eq!(data_plane_async_op_free(0), -2);
data_plane_free_bytes(std::ptr::null(), 0);
assert_eq!(
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
0
);
assert_eq!(
unsafe { data_plane_tcp_bind_start(std::ptr::null(), 0, 0) },
0
);
assert_eq!(unsafe { data_plane_tcp_accept_start(0, 0) }, 0);
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
assert_eq!(
unsafe { data_plane_tcp_write_start(0, std::ptr::null(), 0, 0) },
0
);
assert_eq!(
unsafe { data_plane_udp_bind_start(std::ptr::null(), 0, 0) },
0
);
assert_eq!(
unsafe { data_plane_udp_send_to_start(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
0
);
assert_eq!(unsafe { data_plane_udp_recv_from_start(0, 0, 0) }, 0);
}
}
@@ -472,6 +553,21 @@ fn active_config_server_rejects_data_plane() {
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
-1
);
assert_eq!(
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
0
);
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
set_active_for_test(false);
}
#[cfg(feature = "ffi-dataplane")]
#[test]
fn async_op_invalid_handle_helpers_are_stable() {
assert_eq!(data_plane_async_op_status(u64::MAX), -2);
assert_eq!(data_plane_async_op_wait(u64::MAX, 1), -2);
assert_eq!(data_plane_async_op_cancel(u64::MAX), -2);
assert_eq!(data_plane_async_op_free(u64::MAX), -2);
data_plane_free_bytes(std::ptr::null(), 0);
}