use std::path::Path;
#[derive(Debug, Clone)]
pub struct FfiModule {
pub name: String,
pub lib_path: String,
pub binding_type: &'static str,
pub source_lang: &'static str,
pub target_lang: &'static str,
}
#[derive(Debug, Clone)]
pub struct CrossRef {
pub source_file: String,
pub source_lang: &'static str,
pub target_module: String,
pub target_lang: &'static str,
pub ref_type: &'static str,
pub line: usize,
}
pub trait FfiBinding: Send + Sync {
fn name(&self) -> &'static str;
fn source_lang(&self) -> &'static str;
fn target_lang(&self) -> &'static str;
fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String>;
fn consumer_extensions(&self) -> &[&'static str];
fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool;
}
pub struct PyO3Binding;
impl FfiBinding for PyO3Binding {
fn name(&self) -> &'static str {
"pyo3"
}
fn source_lang(&self) -> &'static str {
"rust"
}
fn target_lang(&self) -> &'static str {
"python"
}
fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
if path.file_name()? != "Cargo.toml" {
return None;
}
if !content.contains("pyo3") && !content.contains("PyO3") {
return None;
}
extract_cargo_crate_name(content)
}
fn consumer_extensions(&self) -> &[&'static str] {
&["py"]
}
fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
let module_name = known_module.replace('-', "_");
import_module == module_name
|| import_module.starts_with(&format!("{}.", module_name))
|| import_name == module_name
}
}
pub struct WasmBindgenBinding;
impl FfiBinding for WasmBindgenBinding {
fn name(&self) -> &'static str {
"wasm-bindgen"
}
fn source_lang(&self) -> &'static str {
"rust"
}
fn target_lang(&self) -> &'static str {
"javascript"
}
fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
if path.file_name()? != "Cargo.toml" {
return None;
}
if !content.contains("wasm-bindgen") {
return None;
}
extract_cargo_crate_name(content)
}
fn consumer_extensions(&self) -> &[&'static str] {
&["js", "ts", "tsx", "mjs"]
}
fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
let module_name = known_module.replace('-', "_");
import_module == module_name
|| import_module.starts_with(&format!("{}.", module_name))
|| import_name == module_name
|| import_module.contains(&format!("/{}", module_name))
}
}
pub struct NapiRsBinding;
impl FfiBinding for NapiRsBinding {
fn name(&self) -> &'static str {
"napi-rs"
}
fn source_lang(&self) -> &'static str {
"rust"
}
fn target_lang(&self) -> &'static str {
"javascript"
}
fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
if path.file_name()? != "Cargo.toml" {
return None;
}
if !content.contains("napi") {
return None;
}
extract_cargo_crate_name(content)
}
fn consumer_extensions(&self) -> &[&'static str] {
&["js", "ts", "tsx", "mjs"]
}
fn matches_import(&self, import_module: &str, import_name: &str, known_module: &str) -> bool {
let module_name = known_module.replace('-', "_");
import_module == module_name
|| import_module.starts_with(&format!("{}.", module_name))
|| import_name == module_name
}
}
pub struct CdylibBinding;
impl FfiBinding for CdylibBinding {
fn name(&self) -> &'static str {
"cdylib"
}
fn source_lang(&self) -> &'static str {
"rust"
}
fn target_lang(&self) -> &'static str {
"c"
}
fn detect_in_build_file(&self, path: &Path, content: &str) -> Option<String> {
if path.file_name()? != "Cargo.toml" {
return None;
}
if !content.contains("cdylib") {
return None;
}
if content.contains("pyo3") || content.contains("wasm-bindgen") || content.contains("napi")
{
return None;
}
extract_cargo_crate_name(content)
}
fn consumer_extensions(&self) -> &[&'static str] {
&["c", "cpp", "h", "hpp", "py"] }
fn matches_import(
&self,
_import_module: &str,
_import_name: &str,
_known_module: &str,
) -> bool {
false
}
}
pub struct CtypesBinding;
impl FfiBinding for CtypesBinding {
fn name(&self) -> &'static str {
"ctypes"
}
fn source_lang(&self) -> &'static str {
"python"
}
fn target_lang(&self) -> &'static str {
"c"
}
fn detect_in_build_file(&self, _path: &Path, _content: &str) -> Option<String> {
None
}
fn consumer_extensions(&self) -> &[&'static str] {
&["py"]
}
fn matches_import(&self, import_module: &str, import_name: &str, _known_module: &str) -> bool {
import_module == "ctypes" || import_name == "ctypes" || import_name == "CDLL"
}
}
pub struct CffiBinding;
impl FfiBinding for CffiBinding {
fn name(&self) -> &'static str {
"cffi"
}
fn source_lang(&self) -> &'static str {
"python"
}
fn target_lang(&self) -> &'static str {
"c"
}
fn detect_in_build_file(&self, _path: &Path, _content: &str) -> Option<String> {
None
}
fn consumer_extensions(&self) -> &[&'static str] {
&["py"]
}
fn matches_import(&self, import_module: &str, import_name: &str, _known_module: &str) -> bool {
import_module == "cffi" || import_name == "cffi" || import_name == "FFI"
}
}
pub struct FfiDetector {
bindings: Vec<Box<dyn FfiBinding>>,
}
impl Default for FfiDetector {
fn default() -> Self {
Self::new()
}
}
impl FfiDetector {
pub fn new() -> Self {
Self {
bindings: vec![
Box::new(PyO3Binding),
Box::new(WasmBindgenBinding),
Box::new(NapiRsBinding),
Box::new(CdylibBinding),
Box::new(CtypesBinding),
Box::new(CffiBinding),
],
}
}
pub fn add_binding(&mut self, binding: Box<dyn FfiBinding>) {
self.bindings.push(binding);
}
pub fn bindings(&self) -> &[Box<dyn FfiBinding>] {
&self.bindings
}
pub fn detect_modules(&self, path: &Path, content: &str) -> Vec<FfiModule> {
let mut modules = Vec::new();
let parent = path.parent().unwrap_or(Path::new(""));
let lib_path = parent.join("src").join("lib.rs");
for binding in &self.bindings {
if let Some(name) = binding.detect_in_build_file(path, content) {
modules.push(FfiModule {
name,
lib_path: lib_path.to_string_lossy().to_string(),
binding_type: binding.name(),
source_lang: binding.source_lang(),
target_lang: binding.target_lang(),
});
}
}
modules
}
pub fn match_import<'a>(
&self,
import_module: &str,
import_name: &str,
known_modules: &'a [FfiModule],
) -> Option<(&'a FfiModule, &'static str)> {
for module in known_modules {
for binding in &self.bindings {
if binding.name() == module.binding_type
&& binding.matches_import(import_module, import_name, &module.name)
{
return Some((module, binding.name()));
}
}
}
None
}
pub fn is_consumer_extension(&self, ext: &str) -> bool {
for binding in &self.bindings {
if binding.consumer_extensions().contains(&ext) {
return true;
}
}
false
}
}
fn extract_cargo_crate_name(content: &str) -> Option<String> {
let mut in_package = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[package]" {
in_package = true;
continue;
}
if trimmed.starts_with('[') {
in_package = false;
continue;
}
if in_package && trimmed.starts_with("name") {
if let Some(eq_pos) = trimmed.find('=') {
let value = trimmed[eq_pos + 1..].trim();
let value = value.trim_matches('"').trim_matches('\'');
return Some(value.to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pyo3_detection() {
let binding = PyO3Binding;
let content = r#"
[package]
name = "my-lib"
version = "0.1.0"
[dependencies]
pyo3 = "0.20"
"#;
let result = binding.detect_in_build_file(Path::new("Cargo.toml"), content);
assert_eq!(result, Some("my-lib".to_string()));
}
#[test]
fn test_pyo3_import_matching() {
let binding = PyO3Binding;
assert!(binding.matches_import("my_lib", "", "my-lib"));
assert!(binding.matches_import("my_lib.submodule", "", "my-lib"));
assert!(!binding.matches_import("other_lib", "", "my-lib"));
}
#[test]
fn test_detector_registry() {
let detector = FfiDetector::new();
assert!(detector.bindings().len() >= 6);
let content = r#"
[package]
name = "wasm-app"
[dependencies]
wasm-bindgen = "0.2"
"#;
let modules = detector.detect_modules(Path::new("Cargo.toml"), content);
assert_eq!(modules.len(), 1);
assert_eq!(modules[0].name, "wasm-app");
assert_eq!(modules[0].binding_type, "wasm-bindgen");
}
}