diff --git a/Cargo.lock b/Cargo.lock index 1f4080e..a8e38f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1993,6 +1993,17 @@ dependencies = [ "zip", ] +[[package]] +name = "easytier-ffi" +version = "0.1.0" +dependencies = [ + "dashmap", + "easytier", + "once_cell", + "serde", + "serde_json", +] + [[package]] name = "easytier-gui" version = "2.2.4" diff --git a/Cargo.toml b/Cargo.toml index 37a29d7..be854f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web", + "easytier-contrib/easytier-ffi", ] default-members = ["easytier", "easytier-web"] diff --git a/easytier-contrib/easytier-ffi/Cargo.toml b/easytier-contrib/easytier-ffi/Cargo.toml new file mode 100644 index 0000000..d86091c --- /dev/null +++ b/easytier-contrib/easytier-ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "easytier-ffi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +easytier = { path = "../../easytier" } + +once_cell = "1.18.0" +dashmap = "6.0" + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" diff --git a/easytier-contrib/easytier-ffi/examples/csharp.cs b/easytier-contrib/easytier-ffi/examples/csharp.cs new file mode 100644 index 0000000..e6cfdf6 --- /dev/null +++ b/easytier-contrib/easytier-ffi/examples/csharp.cs @@ -0,0 +1,159 @@ +public class EasyTierFFI +{ + // 导入 DLL 函数 + private const string DllName = "easytier_ffi.dll"; + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern int parse_config([MarshalAs(UnmanagedType.LPStr)] string cfgStr); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern int run_network_instance([MarshalAs(UnmanagedType.LPStr)] string cfgStr); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern int retain_network_instance(IntPtr instNames, int length); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern int collect_network_infos(IntPtr infos, int maxLength); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern void get_error_msg(out IntPtr errorMsg); + + [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] + private static extern void free_string(IntPtr str); + + // 定义 KeyValuePair 结构体 + [StructLayout(LayoutKind.Sequential)] + public struct KeyValuePair + { + public IntPtr Key; + public IntPtr Value; + } + + // 解析配置 + public static void ParseConfig(string config) + { + if (string.IsNullOrEmpty(config)) + { + throw new ArgumentException("Configuration string cannot be null or empty."); + } + + int result = parse_config(config); + if (result < 0) + { + throw new Exception(GetErrorMessage()); + } + } + + // 启动网络实例 + public static void RunNetworkInstance(string config) + { + if (string.IsNullOrEmpty(config)) + { + throw new ArgumentException("Configuration string cannot be null or empty."); + } + + int result = run_network_instance(config); + if (result < 0) + { + throw new Exception(GetErrorMessage()); + } + } + + // 保留网络实例 + public static void RetainNetworkInstances(string[] instanceNames) + { + IntPtr[] namePointers = null; + IntPtr namesPtr = IntPtr.Zero; + + try + { + if (instanceNames != null && instanceNames.Length > 0) + { + namePointers = new IntPtr[instanceNames.Length]; + for (int i = 0; i < instanceNames.Length; i++) + { + if (string.IsNullOrEmpty(instanceNames[i])) + { + throw new ArgumentException("Instance name cannot be null or empty."); + } + namePointers[i] = Marshal.StringToHGlobalAnsi(instanceNames[i]); + } + + namesPtr = Marshal.AllocHGlobal(Marshal.SizeOf() * namePointers.Length); + Marshal.Copy(namePointers, 0, namesPtr, namePointers.Length); + } + + int result = retain_network_instance(namesPtr, instanceNames?.Length ?? 0); + if (result < 0) + { + throw new Exception(GetErrorMessage()); + } + } + finally + { + if (namePointers != null) + { + foreach (var ptr in namePointers) + { + if (ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(ptr); + } + } + } + + if (namesPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(namesPtr); + } + } + } + + // 收集网络信息 + public static KeyValuePair[] CollectNetworkInfos(int maxLength) + { + IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf() * maxLength); + try + { + int count = collect_network_infos(buffer, maxLength); + if (count < 0) + { + throw new Exception(GetErrorMessage()); + } + + var result = new KeyValuePair[count]; + for (int i = 0; i < count; i++) + { + var kv = Marshal.PtrToStructure(buffer + i * Marshal.SizeOf()); + string key = Marshal.PtrToStringAnsi(kv.Key); + string value = Marshal.PtrToStringAnsi(kv.Value); + + // 释放由 FFI 分配的字符串内存 + free_string(kv.Key); + free_string(kv.Value); + + result[i] = new KeyValuePair(key, value); + } + + return result; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + // 获取错误信息 + private static string GetErrorMessage() + { + get_error_msg(out IntPtr errorMsgPtr); + if (errorMsgPtr == IntPtr.Zero) + { + return "Unknown error"; + } + + string errorMsg = Marshal.PtrToStringAnsi(errorMsgPtr); + free_string(errorMsgPtr); // 释放错误信息字符串 + return errorMsg; + } +} diff --git a/easytier-contrib/easytier-ffi/src/lib.rs b/easytier-contrib/easytier-ffi/src/lib.rs new file mode 100644 index 0000000..ab8a7b1 --- /dev/null +++ b/easytier-contrib/easytier-ffi/src/lib.rs @@ -0,0 +1,199 @@ +use std::sync::Mutex; + +use dashmap::DashMap; +use easytier::{ + common::config::{ConfigLoader as _, TomlConfigLoader}, + launcher::NetworkInstance, +}; + +static INSTANCE_MAP: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(DashMap::new); + +static ERROR_MSG: once_cell::sync::Lazy>> = + once_cell::sync::Lazy::new(|| Mutex::new(Vec::new())); + +#[repr(C)] +pub struct KeyValuePair { + pub key: *const std::ffi::c_char, + pub value: *const std::ffi::c_char, +} + +fn set_error_msg(msg: &str) { + let bytes = msg.as_bytes(); + let mut msg_buf = ERROR_MSG.lock().unwrap(); + let len = bytes.len(); + msg_buf.resize(len, 0); + msg_buf[..len].copy_from_slice(bytes); +} + +#[no_mangle] +pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) { + let msg_buf = ERROR_MSG.lock().unwrap(); + if msg_buf.is_empty() { + unsafe { + *out = std::ptr::null(); + } + return; + } + let cstr = std::ffi::CString::new(&msg_buf[..]).unwrap(); + unsafe { + *out = cstr.into_raw(); + } +} + +#[no_mangle] +pub extern "C" fn free_string(s: *const std::ffi::c_char) { + if s.is_null() { + return; + } + unsafe { + let _ = std::ffi::CString::from_raw(s as *mut std::ffi::c_char); + } +} + +#[no_mangle] +pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int { + let cfg_str = unsafe { + assert!(!cfg_str.is_null()); + std::ffi::CStr::from_ptr(cfg_str) + .to_string_lossy() + .into_owned() + }; + + if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) { + set_error_msg(&format!("failed to parse config: {:?}", e)); + return -1; + } + + 0 +} + +#[no_mangle] +pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int { + let cfg_str = unsafe { + assert!(!cfg_str.is_null()); + std::ffi::CStr::from_ptr(cfg_str) + .to_string_lossy() + .into_owned() + }; + let cfg = match TomlConfigLoader::new_from_str(&cfg_str) { + Ok(cfg) => cfg, + Err(e) => { + set_error_msg(&format!("failed to parse config: {}", e)); + return -1; + } + }; + + let inst_name = cfg.get_inst_name(); + + if INSTANCE_MAP.contains_key(&inst_name) { + set_error_msg("instance already exists"); + return -1; + } + + let mut instance = NetworkInstance::new(cfg); + if let Err(e) = instance.start().map_err(|e| e.to_string()) { + set_error_msg(&format!("failed to start instance: {}", e)); + return -1; + } + + INSTANCE_MAP.insert(inst_name, instance); + + 0 +} + +#[no_mangle] +pub extern "C" fn retain_network_instance( + inst_names: *const *const std::ffi::c_char, + length: usize, +) -> std::ffi::c_int { + if length == 0 { + INSTANCE_MAP.clear(); + 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::>() + }; + + let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k)); + + 0 +} + +#[no_mangle] +pub extern "C" fn collect_network_infos( + infos: *mut KeyValuePair, + max_length: usize, +) -> std::ffi::c_int { + if max_length == 0 { + return 0; + } + + let infos = unsafe { + assert!(!infos.is_null()); + std::slice::from_raw_parts_mut(infos, max_length) + }; + + let mut index = 0; + for instance in INSTANCE_MAP.iter() { + if index >= max_length { + break; + } + let key = instance.key(); + let Some(value) = instance.get_running_info() else { + continue; + }; + // convert value to json string + let value = match serde_json::to_string(&value) { + Ok(value) => value, + Err(e) => { + set_error_msg(&format!("failed to serialize instance info: {}", e)); + return -1; + } + }; + + infos[index] = KeyValuePair { + key: std::ffi::CString::new(key.clone()).unwrap().into_raw(), + value: std::ffi::CString::new(value).unwrap().into_raw(), + }; + index += 1; + } + + index as std::ffi::c_int +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_config() { + let cfg_str = r#" + inst_name = "test" + network = "test_network" + fdsafdsa + "#; + let cstr = std::ffi::CString::new(cfg_str).unwrap(); + assert_eq!(parse_config(cstr.as_ptr()), 0); + } + + #[test] + fn test_run_network_instance() { + let cfg_str = r#" + inst_name = "test" + network = "test_network" + "#; + let cstr = std::ffi::CString::new(cfg_str).unwrap(); + assert_eq!(run_network_instance(cstr.as_ptr()), 0); + } +} diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index b4e8e53..5738c5e 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -274,20 +274,23 @@ impl TomlConfigLoader { config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default())); - Ok(TomlConfigLoader { + let config = TomlConfigLoader { config: Arc::new(Mutex::new(config)), - }) + }; + + let old_ns = config.get_network_identity(); + config.set_network_identity(NetworkIdentity::new( + old_ns.network_name, + old_ns.network_secret.unwrap_or_default(), + )); + + Ok(config) } pub fn new(config_path: &PathBuf) -> Result { let config_str = std::fs::read_to_string(config_path) .with_context(|| format!("failed to read config file: {:?}", config_path))?; let ret = Self::new_from_str(&config_str)?; - let old_ns = ret.get_network_identity(); - ret.set_network_identity(NetworkIdentity::new( - old_ns.network_name, - old_ns.network_secret.unwrap_or_default(), - )); Ok(ret) }