mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-13 21:27:25 +08:00
introduce ffi for easytier (#791)
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -1993,6 +1993,17 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "easytier-ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dashmap",
|
||||||
|
"easytier",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.2.4"
|
version = "2.2.4"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"easytier-gui/src-tauri",
|
"easytier-gui/src-tauri",
|
||||||
"easytier-rpc-build",
|
"easytier-rpc-build",
|
||||||
"easytier-web",
|
"easytier-web",
|
||||||
|
"easytier-contrib/easytier-ffi",
|
||||||
]
|
]
|
||||||
default-members = ["easytier", "easytier-web"]
|
default-members = ["easytier", "easytier-web"]
|
||||||
|
|
||||||
|
|||||||
16
easytier-contrib/easytier-ffi/Cargo.toml
Normal file
16
easytier-contrib/easytier-ffi/Cargo.toml
Normal file
@@ -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"
|
||||||
159
easytier-contrib/easytier-ffi/examples/csharp.cs
Normal file
159
easytier-contrib/easytier-ffi/examples/csharp.cs
Normal file
@@ -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<IntPtr>() * 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<string, string>[] CollectNetworkInfos(int maxLength)
|
||||||
|
{
|
||||||
|
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf<KeyValuePair>() * maxLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int count = collect_network_infos(buffer, maxLength);
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
throw new Exception(GetErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new KeyValuePair<string, string>[count];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var kv = Marshal.PtrToStructure<KeyValuePair>(buffer + i * Marshal.SizeOf<KeyValuePair>());
|
||||||
|
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<string, string>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
199
easytier-contrib/easytier-ffi/src/lib.rs
Normal file
199
easytier-contrib/easytier-ffi/src/lib.rs
Normal file
@@ -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<DashMap<String, NetworkInstance>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
|
static ERROR_MSG: once_cell::sync::Lazy<Mutex<Vec<u8>>> =
|
||||||
|
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::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,20 +274,23 @@ impl TomlConfigLoader {
|
|||||||
|
|
||||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||||
|
|
||||||
Ok(TomlConfigLoader {
|
let config = TomlConfigLoader {
|
||||||
config: Arc::new(Mutex::new(config)),
|
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<Self, anyhow::Error> {
|
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
|
||||||
let config_str = std::fs::read_to_string(config_path)
|
let config_str = std::fs::read_to_string(config_path)
|
||||||
.with_context(|| format!("failed to read config file: {:?}", config_path))?;
|
.with_context(|| format!("failed to read config file: {:?}", config_path))?;
|
||||||
let ret = Self::new_from_str(&config_str)?;
|
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)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user