use std::ffi::{CStr, c_char, c_int, c_long};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use log::{error, info};
use parking_lot::Mutex;
use std::sync::OnceLock;
use tokio::sync::oneshot;
use crate::logging::{DynamicFileLogWriter, LogWriter};
use super::common::{
self, INITIALIZED, LOG_FILE, LOGGER_INITIALIZED, TUN_SERVICE, TunServiceHandle, setup_log_file,
};
pub type ProtectSocketCallback = extern "C" fn(fd: c_int) -> bool;
static PROTECT_CALLBACK: OnceLock<Mutex<Option<ProtectSocketCallback>>> = OnceLock::new();
struct IosSocketProtector;
impl crate::tun::SocketProtector for IosSocketProtector {
fn protect(&self, fd: std::os::unix::io::RawFd) -> std::io::Result<()> {
let callback_guard = PROTECT_CALLBACK.get_or_init(|| Mutex::new(None)).lock();
if let Some(callback) = *callback_guard {
if callback(fd as c_int) {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Socket protection failed",
))
}
} else {
Ok(())
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn shoes_init(log_level: *const c_char) -> c_int {
if INITIALIZED.swap(true, Ordering::SeqCst) {
return 0;
}
let level_str = if log_level.is_null() {
"info"
} else {
match CStr::from_ptr(log_level).to_str() {
Ok(s) => s,
Err(_) => "info",
}
};
let filter = crate::logging::parse_log_level(level_str).unwrap_or(log::LevelFilter::Info);
if !LOGGER_INITIALIZED.swap(true, Ordering::SeqCst) {
LOG_FILE.get_or_init(|| parking_lot::Mutex::new(None));
let writers: Vec<Box<dyn LogWriter>> = vec![Box::new(DynamicFileLogWriter::new(&LOG_FILE))];
let directives = vec![crate::logging::Directive {
name: None,
level: filter,
}];
crate::logging::init_multi_logger(writers, directives);
}
TUN_SERVICE.get_or_init(|| Mutex::new(None));
PROTECT_CALLBACK.get_or_init(|| Mutex::new(None));
info!("shoes iOS initialized with log level: {}", level_str);
0
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn shoes_start(
config_yaml: *const c_char,
protect_callback: ProtectSocketCallback,
) -> c_long {
let _lifecycle = common::SERVICE_LIFECYCLE.lock();
if TUN_SERVICE
.get()
.is_some_and(|service| service.lock().is_some())
{
error!("Stop the previous shoes service before starting another");
return -1;
}
if config_yaml.is_null() {
error!("shoes_start: config_yaml is null");
return -1;
}
let config_str = match CStr::from_ptr(config_yaml).to_str() {
Ok(s) => s.to_string(),
Err(e) => {
error!("shoes_start: invalid UTF-8 in config_yaml: {}", e);
return -1;
}
};
info!("shoes_start: config length = {} bytes", config_str.len());
{
let mut callback_guard = PROTECT_CALLBACK.get_or_init(|| Mutex::new(None)).lock();
*callback_guard = Some(protect_callback);
}
crate::tun::set_global_socket_protector(Arc::new(IosSocketProtector));
crate::thread_util::set_num_threads(2);
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(2)
.build()
{
Ok(rt) => rt,
Err(e) => {
error!("shoes_start: failed to create runtime: {}", e);
crate::tun::clear_global_socket_protector();
return -1;
}
};
let (shutdown_tx, shutdown_rx) = oneshot::channel();
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let task = common::spawn_service(&runtime, config_str, shutdown_rx, running.clone());
let handle = TunServiceHandle {
shutdown_tx: Some(shutdown_tx),
running,
runtime,
task,
};
let mut guard = TUN_SERVICE.get_or_init(|| Mutex::new(None)).lock();
*guard = Some(handle);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn shoes_stop(_handle: c_long) {
common::stop_service();
if let Some(callback) = PROTECT_CALLBACK.get() {
let mut guard = callback.lock();
*guard = None;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn shoes_is_running() -> bool {
common::is_service_running()
}
#[unsafe(no_mangle)]
pub extern "C" fn shoes_get_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn shoes_set_log_file(path: *const c_char) -> c_int {
if path.is_null() {
return -1;
}
let path_str = match CStr::from_ptr(path).to_str() {
Ok(s) => s,
Err(_) => return -1,
};
setup_log_file(path_str)
}