Add FFI JNI JSON RPC bridge (#2326)

* Add FFI JNI JSON RPC bridge
* Add FFI instance list API
This commit is contained in:
KKRainbow
2026-06-07 17:48:58 +08:00
committed by GitHub
parent da28c8badc
commit 9d965cae64
14 changed files with 965 additions and 9 deletions
@@ -0,0 +1,91 @@
use std::{
ffi::{CStr, c_char},
ptr,
};
use easytier_ffi::{call_json_rpc, free_string};
use jni::JNIEnv;
use jni::objects::{JClass, JString};
use jni::sys::jstring;
use crate::{
error::{get_last_error, throw_exception},
strings::{jstring_to_cstring, optional_jstring_to_cstring},
};
pub(crate) fn call_json_rpc_jni(
mut env: JNIEnv,
_class: JClass,
service_name: JString,
method_name: JString,
domain_name: JString,
payload_json: JString,
) -> jstring {
let service_name_cstr = match jstring_to_cstring(&mut env, &service_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid service name: {}", e));
return ptr::null_mut();
}
};
let method_name_cstr = match jstring_to_cstring(&mut env, &method_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid method name: {}", e));
return ptr::null_mut();
}
};
let domain_name_cstr = match optional_jstring_to_cstring(&mut env, &domain_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid domain name: {}", e));
return ptr::null_mut();
}
};
let payload_json_cstr = match jstring_to_cstring(&mut env, &payload_json) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid payload JSON: {}", e));
return ptr::null_mut();
}
};
let domain_name_ptr = domain_name_cstr
.as_ref()
.map_or(ptr::null(), |cstr| cstr.as_ptr());
let mut response_ptr: *const c_char = ptr::null();
let result = unsafe {
call_json_rpc(
service_name_cstr.as_ptr(),
method_name_cstr.as_ptr(),
domain_name_ptr,
payload_json_cstr.as_ptr(),
&mut response_ptr,
)
};
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
if response_ptr.is_null() {
throw_exception(&mut env, "JSON RPC returned a null response");
return ptr::null_mut();
}
let response = unsafe { CStr::from_ptr(response_ptr) }
.to_string_lossy()
.into_owned();
free_string(response_ptr);
match env.new_string(&response) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create JSON RPC response string");
ptr::null_mut()
}
}
}
@@ -9,7 +9,9 @@
//! - `parseConfig(config)`: validate TOML config text.
//! - `runNetworkInstance(config)`: start a local network instance.
//! - `retainNetworkInstance(instanceNames)`: retain named instances and stop the rest.
//! - `listInstances()`: return running instance names and IDs as JSON.
//! - `collectNetworkInfos()`: return running instance info as a JSON string.
//! - `callJsonRpc(...)`: call an exposed EasyTier RPC service with JSON payload.
//!
//! Config server client APIs:
//! - `startConfigServerClient(url, hostname, machineId, secureMode, callback)`:
@@ -28,6 +30,7 @@ mod callback;
mod config_server_api;
mod data_plane_api;
mod error;
mod json_rpc_api;
mod logger;
mod network_api;
mod strings;
@@ -126,6 +129,53 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
network_api::collect_network_infos_jni(env, class, max_length)
}
/// List running network instance names and IDs.
///
/// Java signature:
/// `EasyTierJNI.listInstances(maxLength: Int): String?`
///
/// Returns a JSON object whose keys are instance names and whose values are
/// instance ID strings. On failure this returns null and throws
/// `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_listInstances(
env: JNIEnv,
class: JClass,
max_length: jint,
) -> jstring {
logger::init();
network_api::list_instances_jni(env, class, max_length)
}
/// Call an exposed EasyTier RPC method using protobuf JSON.
///
/// Java signature:
/// `EasyTierJNI.callJsonRpc(serviceName, methodName, domainName, payloadJson): String?`
///
/// Instance lifecycle management RPCs are intentionally not exposed here. Use
/// the dedicated EasyTierJNI instance APIs for start/retain/delete/collect.
/// `payloadJson` must include any `instance` selector required by the target
/// RPC. On failure this returns null and throws `RuntimeException`.
#[unsafe(no_mangle)]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_callJsonRpc(
env: JNIEnv,
class: JClass,
service_name: JString,
method_name: JString,
domain_name: JString,
payload_json: JString,
) -> jstring {
logger::init();
json_rpc_api::call_json_rpc_jni(
env,
class,
service_name,
method_name,
domain_name,
payload_json,
)
}
/// Return the latest FFI/JNI error string for the calling thread.
///
/// Java signature:
@@ -2,8 +2,8 @@ use std::{ffi::CStr, ptr};
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
use easytier_ffi::{
KeyValuePair, collect_network_infos, parse_config, retain_network_instance,
run_network_instance, set_tun_fd,
KeyValuePair, collect_network_infos, free_string, list_instance, parse_config,
retain_network_instance, run_network_instance, set_tun_fd,
};
use jni::JNIEnv;
use jni::objects::{JClass, JObjectArray, JString};
@@ -190,16 +190,18 @@ pub(crate) fn collect_network_infos_jni(
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy();
let val = CStr::from_ptr(val_ptr).to_string_lossy();
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
free_string(key_ptr);
free_string(val_ptr);
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(&val) {
Ok(v) => v,
Err(_) => {
throw_exception(&mut env, "Failed to parse JSON");
continue;
}
};
ret.map.insert(key.to_string(), value);
ret.map.insert(key, value);
}
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
@@ -212,3 +214,48 @@ pub(crate) fn collect_network_infos_jni(
}
}
}
pub(crate) fn list_instances_jni(mut env: JNIEnv, _class: JClass, max_length: jint) -> jstring {
let max_length = max_length.max(0) as usize;
let mut infos = vec![
KeyValuePair {
key: ptr::null(),
value: ptr::null(),
};
max_length
];
unsafe {
let count = list_instance(infos.as_mut_ptr(), max_length);
if count < 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
let mut ret = serde_json::Map::new();
for info in infos.iter().take(count as usize) {
let key_ptr = info.key;
let val_ptr = info.value;
if key_ptr.is_null() || val_ptr.is_null() {
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy().into_owned();
let val = CStr::from_ptr(val_ptr).to_string_lossy().into_owned();
free_string(key_ptr);
free_string(val_ptr);
ret.insert(key, serde_json::Value::String(val));
}
let json_str = serde_json::Value::Object(ret).to_string();
match env.new_string(&json_str) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create instance list JSON string");
ptr::null_mut()
}
}
}
}