use anyhow::{Context, anyhow, bail};
use crate::ts_syn::abi::{MacroContextIR, MacroResult};
#[cfg(not(target_arch = "wasm32"))]
type FfiRunFn = unsafe extern "C" fn(
ctx_ptr: *const u8,
ctx_len: usize,
out_ptr: *mut *mut u8,
out_len: *mut usize,
) -> i32;
#[cfg(not(target_arch = "wasm32"))]
type FfiManifestFn = unsafe extern "C" fn(out_ptr: *mut *mut u8, out_len: *mut usize) -> i32;
#[cfg(not(target_arch = "wasm32"))]
type FfiFreeFn = unsafe extern "C" fn(ptr: *mut u8, len: usize);
pub(crate) struct ExternalMacroLoader {
#[cfg(not(target_arch = "wasm32"))]
root_dir: std::path::PathBuf,
#[cfg(not(target_arch = "wasm32"))]
loaded_libs: std::sync::Mutex<std::collections::HashMap<String, libloading::Library>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl ExternalMacroLoader {
pub(crate) fn new(root_dir: std::path::PathBuf) -> Self {
Self {
root_dir,
loaded_libs: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
pub(crate) fn resolve_decorator_names(&self, package_path: &str) -> Vec<String> {
let lib_path = match self.find_native_lib(package_path) {
Some(p) => p,
None => return Vec::new(),
};
let mut libs = match self.loaded_libs.lock() {
Ok(l) => l,
Err(_) => return Vec::new(),
};
let lib = if let Some(lib) = libs.get(package_path) {
lib
} else {
match unsafe { libloading::Library::new(&lib_path) } {
Ok(loaded) => {
libs.insert(package_path.to_string(), loaded);
libs.get(package_path).unwrap()
}
Err(_) => return Vec::new(),
}
};
let manifest_fn: libloading::Symbol<FfiManifestFn> =
match unsafe { lib.get(b"__macroforge_ffi_get_manifest") } {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let free_fn: libloading::Symbol<FfiFreeFn> =
match unsafe { lib.get(b"__macroforge_ffi_free") } {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let mut out_ptr: *mut u8 = std::ptr::null_mut();
let mut out_len: usize = 0;
let status = unsafe { manifest_fn(&mut out_ptr, &mut out_len) };
if status != 0 || out_ptr.is_null() || out_len == 0 {
return Vec::new();
}
let json = unsafe {
let slice = std::slice::from_raw_parts(out_ptr, out_len);
let s = String::from_utf8_lossy(slice).into_owned();
free_fn(out_ptr, out_len);
s
};
let manifest: serde_json::Value = match serde_json::from_str(&json) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
manifest
.get("decorators")
.and_then(|d| d.as_array())
.map(|arr| {
arr.iter()
.filter_map(|d| d.get("export").and_then(|e| e.as_str()).map(String::from))
.collect()
})
.unwrap_or_default()
}
pub(crate) fn run_macro(&self, ctx: &MacroContextIR) -> anyhow::Result<MacroResult> {
if let Some(result) = self.try_run_ffi(ctx)? {
return Ok(result);
}
bail!(
"External macro '{}' from '{}' not found via FFI. \
Ensure the package exports FFI symbols (built with #[ts_macro_derive]).",
ctx.macro_name,
ctx.module_path
)
}
fn try_run_ffi(&self, ctx: &MacroContextIR) -> anyhow::Result<Option<MacroResult>> {
use convert_case::{Case, Casing};
let ffi_symbol = format!(
"__macroforge_ffi_run_{}",
ctx.macro_name.to_case(Case::Snake)
);
let ctx_json =
serde_json::to_string(ctx).map_err(|e| anyhow!("Failed to serialize context: {e}"))?;
let lib_path = match self.find_native_lib(&ctx.module_path) {
Some(p) => p,
None => return Ok(None),
};
let mut libs = self
.loaded_libs
.lock()
.map_err(|e| anyhow!("Lock poisoned: {e}"))?;
let lib = if let Some(lib) = libs.get(&ctx.module_path) {
lib
} else {
let loaded = unsafe { libloading::Library::new(&lib_path) }
.map_err(|e| anyhow!("Failed to load {}: {e}", lib_path.display()))?;
libs.insert(ctx.module_path.clone(), loaded);
libs.get(&ctx.module_path).unwrap()
};
let run_fn: libloading::Symbol<FfiRunFn> = match unsafe { lib.get(ffi_symbol.as_bytes()) } {
Ok(f) => f,
Err(_) => return Ok(None), };
let free_fn: libloading::Symbol<FfiFreeFn> =
match unsafe { lib.get(b"__macroforge_ffi_free") } {
Ok(f) => f,
Err(_) => return Ok(None),
};
let mut out_ptr: *mut u8 = std::ptr::null_mut();
let mut out_len: usize = 0;
let status = unsafe {
run_fn(
ctx_json.as_ptr(),
ctx_json.len(),
&mut out_ptr,
&mut out_len,
)
};
if out_ptr.is_null() || out_len == 0 {
bail!("FFI macro returned null output");
}
let output = unsafe {
let slice = std::slice::from_raw_parts(out_ptr, out_len);
let s = String::from_utf8_lossy(slice).into_owned();
free_fn(out_ptr, out_len);
s
};
if status != 0 {
bail!("External macro FFI error: {output}");
}
let result: MacroResult =
serde_json::from_str(&output).context("Failed to parse FFI macro result")?;
Ok(Some(result))
}
fn find_native_lib(&self, module_path: &str) -> Option<std::path::PathBuf> {
let node_modules = self.root_dir.join("node_modules");
let pkg_dir = node_modules.join(module_path);
if !pkg_dir.is_dir() {
return None;
}
let entries = std::fs::read_dir(&pkg_dir).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext == "node" || ext == "dylib" || ext == "so" {
return Some(path);
}
}
}
None
}
}
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use js_sys;
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use wasm_bindgen::prelude::*;
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
static EXTERNAL_CALLBACKS: std::sync::OnceLock<ExternalCallbacks> = std::sync::OnceLock::new();
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
struct ExternalCallbacks {
resolve: js_sys::Function,
run: js_sys::Function,
}
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = "setupExternalMacros")]
pub fn setup_external_macros(resolve: js_sys::Function, run: js_sys::Function) {
let _ = EXTERNAL_CALLBACKS.set(ExternalCallbacks { resolve, run });
}
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
impl ExternalMacroLoader {
pub(crate) fn new(_root_dir: std::path::PathBuf) -> Self {
Self {}
}
pub(crate) fn resolve_decorator_names(&self, package_path: &str) -> Vec<String> {
let Some(callbacks) = EXTERNAL_CALLBACKS.get() else {
return Vec::new();
};
let this = JsValue::NULL;
let pkg = JsValue::from_str(package_path);
match callbacks.resolve.call1(&this, &pkg) {
Ok(result) => serde_wasm_bindgen::from_value(result).unwrap_or_default(),
Err(_) => Vec::new(),
}
}
pub(crate) fn run_macro(&self, ctx: &MacroContextIR) -> anyhow::Result<MacroResult> {
let Some(callbacks) = EXTERNAL_CALLBACKS.get() else {
bail!("External macros callbacks not initialized. Call setupExternalMacros() first.");
};
let ctx_json =
serde_json::to_string(ctx).map_err(|e| anyhow!("Failed to serialize context: {e}"))?;
let this = JsValue::NULL;
let arg = JsValue::from_str(&ctx_json);
let result = callbacks
.run
.call1(&this, &arg)
.map_err(|e| anyhow!("JS callback failed: {:?}", e))?;
if result.is_null() || result.is_undefined() {
bail!("External macro returned null or undefined");
}
if let Some(s) = result.as_string() {
if s.starts_with("Error:") {
bail!("External macro error: {}", s);
}
let host_result: crate::ts_syn::abi::MacroResult =
serde_json::from_str(&s).context("Failed to parse macro result from string")?;
return Ok(host_result);
}
let host_result: crate::ts_syn::abi::MacroResult =
serde_wasm_bindgen::from_value(result).context("Failed to parse macro result")?;
Ok(host_result)
}
}
pub(crate) fn resolve_external_decorator_names(
source: &str,
loader: Option<&ExternalMacroLoader>,
) -> Vec<String> {
let mut all_names = Vec::new();
if let Some(loader) = loader {
let mut search_start = 0;
while let Some(idx) = source[search_start..].find("import macro") {
let abs_idx = search_start + idx;
if let Some(from_idx) = source[abs_idx..].find("from") {
let from_abs = abs_idx + from_idx;
let rest = &source[from_abs + 4..].trim_start();
if rest.starts_with('"') || rest.starts_with('\'') {
let q = rest.chars().next().unwrap();
if let Some(end_quote) = rest[1..].find(q) {
let pkg = &rest[1..end_quote + 1];
let names = loader.resolve_decorator_names(pkg);
all_names.extend(names);
}
}
}
search_start = abs_idx + 12; }
}
all_names
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm")))]
impl ExternalMacroLoader {
pub(crate) fn new(_root_dir: std::path::PathBuf) -> Self {
Self {}
}
pub(crate) fn resolve_decorator_names(&self, _package_path: &str) -> Vec<String> {
Vec::new()
}
pub(crate) fn run_macro(&self, _ctx: &MacroContextIR) -> anyhow::Result<MacroResult> {
bail!("External macros are not supported in this WASM build (wasm feature disabled)")
}
}