use std::ffi::{CStr, CString, c_char};
use crate::data::types::{Gender, Language, LeapMonth};
use crate::dto::{error_json as build_error_json, parse_config_json};
use crate::error::BridgeError;
fn parse_gender(s: &str) -> Result<Gender, BridgeError> {
match s.to_lowercase().as_str() {
"male" => Ok(Gender::Male),
"female" => Ok(Gender::Female),
_ => Err(BridgeError::invalid_argument(format!(
"invalid gender '{s}': expected 'male' or 'female'"
))),
}
}
fn parse_language(s: &str) -> Result<Language, BridgeError> {
Language::from_code(s).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid language '{s}': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN"
))
})
}
unsafe fn cstr_to_str<'a>(ptr: *const c_char, param_name: &str) -> Result<&'a str, BridgeError> {
if ptr.is_null() {
return Err(BridgeError::invalid_argument(format!(
"invalid {param_name}: pointer is null"
)));
}
unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| {
BridgeError::invalid_argument(format!("invalid {param_name}: not valid UTF-8: {e}"))
})
}
unsafe fn cstr_to_opt_str<'a>(
ptr: *const c_char,
param_name: &str,
) -> Result<Option<&'a str>, BridgeError> {
if ptr.is_null() {
return Ok(None);
}
unsafe { cstr_to_str(ptr, param_name) }.map(Some)
}
fn catch(
f: impl FnOnce() -> Result<String, BridgeError> + std::panic::UnwindSafe,
) -> Result<String, BridgeError> {
std::panic::catch_unwind(f).unwrap_or_else(|panic| {
Err(BridgeError::internal(crate::dto::panic_message(
panic.as_ref(),
)))
})
}
fn error_json(err: &BridgeError) -> *mut c_char {
CString::new(build_error_json(err)).unwrap().into_raw()
}
fn ok_json(json: String) -> *mut c_char {
CString::new(json).unwrap().into_raw()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_by_solar(
solar_date: *const c_char,
time_index: u8,
gender: *const c_char,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
) -> *mut c_char {
let parsed = (|| -> Result<_, BridgeError> {
let solar_date = unsafe { cstr_to_str(solar_date, "solar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
Ok((
solar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
))
})();
let result = parsed.and_then(|(solar_date, gender, language, config)| {
catch(move || {
Ok(crate::by_solar_json(
solar_date, time_index, gender, fix_leap, language, config,
)?)
})
});
match result {
Ok(json) => ok_json(json),
Err(err) => error_json(&err),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_by_lunar(
lunar_date: *const c_char,
time_index: u8,
gender: *const c_char,
is_leap_month: bool,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
) -> *mut c_char {
let parsed = (|| -> Result<_, BridgeError> {
let lunar_date = unsafe { cstr_to_str(lunar_date, "lunar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
Ok((
lunar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
))
})();
let result = parsed.and_then(|(lunar_date, gender, language, config)| {
catch(move || {
Ok(crate::by_lunar_json(
lunar_date,
time_index,
gender,
LeapMonth::from_flags(is_leap_month, fix_leap),
language,
config,
)?)
})
});
match result {
Ok(json) => ok_json(json),
Err(err) => error_json(&err),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_get_horoscope(
solar_date: *const c_char,
time_index: u8,
gender: *const c_char,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
target_date: *const c_char,
target_time_index: u8,
) -> *mut c_char {
let parsed = (|| -> Result<_, BridgeError> {
let solar_date = unsafe { cstr_to_str(solar_date, "solar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
let target_date = unsafe { cstr_to_str(target_date, "target_date")? };
Ok((
solar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
target_date,
))
})();
let result = parsed.and_then(|(solar_date, gender, language, config, target_date)| {
catch(move || {
let astrolabe =
crate::by_solar(solar_date, time_index, gender, fix_leap, language, config)?;
let horoscope =
crate::get_horoscope(&astrolabe, target_date, target_time_index, language)?;
serde_json::to_string(&horoscope.to_dto(language))
.map_err(|e| BridgeError::internal(format!("failed to serialize horoscope: {e}")))
})
});
match result {
Ok(json) => ok_json(json),
Err(err) => error_json(&err),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_query(query_json: *const c_char) -> *mut c_char {
let parsed = (|| -> Result<_, BridgeError> {
let raw = unsafe { cstr_to_str(query_json, "query_json")? };
serde_json::from_str::<crate::bridge::QueryInput>(raw)
.map_err(|e| BridgeError::invalid_argument(format!("invalid query JSON: {e}")))
})();
let result = parsed.and_then(|input| {
catch(move || {
let value = crate::bridge::query(&input)?;
serde_json::to_string(&serde_json::json!({ "value": value }))
.map_err(|e| BridgeError::internal(format!("failed to serialize result: {e}")))
})
});
match result {
Ok(json) => ok_json(json),
Err(err) => error_json(&err),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}