Skip to content

Commit ffd90c6

Browse files
committed
refactor(ffi): extract ABI registry into a dedicated ffi module
Move per-ABI classification logic out of the extractors and the bridge resolver into a new crate-private `ffi` module. Each ABI now lives in its own spec file (`ffi/c.rs`, `ffi/jni.rs`, `ffi/python.rs`, `ffi/wasm.rs`, `ffi/node_api.rs`), with a thin `ffi/spec.rs` wiring them into the three shared entry-points consumed by the rest of the crate (`rust_exports`, `c_name_export_abi`, `consumers`). Consequences: - `src/extract/rust.rs`: inline attribute-parsing block replaced by a call to `ffi::rust_exports`; `first_quoted` helper removed. - `src/extract/c.rs`: JNI hard-code replaced by `ffi::c_name_export_abi`. - `src/graph/types.rs`: `FfiAbi::consumers` method removed; the same data now lives in the per-ABI spec files. - `src/resolve/ffi_bridge.rs` (flat file) replaced by `src/resolve/ffi_bridge/` directory (`mod.rs` + `resolver.rs`). Adding an ABI now requires one spec file plus one `SPECS` entry — no growing match arms in extractors or inline resolver blocks.
1 parent 0bb1b00 commit ffd90c6

13 files changed

Lines changed: 227 additions & 146 deletions

File tree

src/extract/c.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use tree_sitter::{Node, Parser};
1919

2020
use crate::error::{CodegraphError, Result};
2121
use crate::graph::types::{
22-
Binding, BindingKind, ByteSpan, EntryPoint, FfiAbi, FfiExport, FileFacts, RefRole, Reference,
23-
Scope, ScopeId, ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
22+
Binding, BindingKind, ByteSpan, EntryPoint, FfiExport, FileFacts, RefRole, Reference, Scope,
23+
ScopeId, ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
2424
};
2525
use crate::lang::Language;
2626
use crate::symbol::Descriptor;
@@ -118,17 +118,20 @@ fn c_namespaces(file: &str) -> Vec<String> {
118118
.collect()
119119
}
120120

121-
/// Emit a JNI [`FfiExport`] for each function whose name follows the `Java_*`
122-
/// mangling — the common case where a Java `native` method's implementation is
121+
/// Emit an [`FfiExport`] for each function whose name carries a by-name ABI
122+
/// classification ([`crate::ffi::c_name_export_abi`]) — today the `Java_*`
123+
/// mangling, the common case where a Java `native` method's implementation is
123124
/// written in C. The resolver bridges it to the declaring Java method.
124125
fn jni_exports(symbols: &[Symbol]) -> Vec<FfiExport> {
125126
symbols
126127
.iter()
127-
.filter(|s| s.kind == SymbolKind::Function && s.name.starts_with("Java_"))
128-
.map(|s| FfiExport {
129-
symbol: s.id.clone(),
130-
abi: FfiAbi::Jni,
131-
export_name: s.name.clone(),
128+
.filter(|s| s.kind == SymbolKind::Function)
129+
.filter_map(|s| {
130+
crate::ffi::c_name_export_abi(&s.name).map(|abi| FfiExport {
131+
symbol: s.id.clone(),
132+
abi,
133+
export_name: s.name.clone(),
134+
})
132135
})
133136
.collect()
134137
}

src/extract/rust.rs

Lines changed: 7 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -577,104 +577,21 @@ fn collect_ffi_exports(root: &Node, bytes: &[u8], defs: &[Symbol]) -> Vec<FfiExp
577577
/// The FFI exports a function declares, derived from its attributes.
578578
///
579579
/// In tree-sitter-rust an item's outer attributes are its **preceding siblings**
580-
/// (not children), so we walk back over the run of `attribute_item` nodes.
581-
/// Detection reads attribute text, so it is robust to spelling variants
582-
/// (`#[no_mangle]` vs `#[unsafe(no_mangle)]`).
580+
/// (not children), so we walk back over the run of `attribute_item` nodes,
581+
/// collecting their texts. The marker→ABI classification lives in the neutral
582+
/// per-ABI registry ([`crate::ffi::rust_exports`]); reading attribute text keeps
583+
/// it robust to spelling variants (`#[no_mangle]` vs `#[unsafe(no_mangle)]`).
583584
fn fn_ffi_exports(func: &Node, bytes: &[u8], fn_name: &str) -> Vec<(FfiAbi, String)> {
584-
let mut c_no_mangle = false;
585-
let mut c_override: Option<String> = None;
586-
let mut py = false;
587-
let mut py_override: Option<String> = None;
588-
let mut wasm = false;
589-
let mut wasm_override: Option<String> = None;
590-
let mut napi = false;
591-
let mut napi_override: Option<String> = None;
592-
585+
let mut attr_texts: Vec<&str> = Vec::new();
593586
let mut sib = func.prev_sibling();
594587
while let Some(node) = sib {
595588
if node.kind() != "attribute_item" {
596589
break;
597590
}
598-
let text = node_text(&node, bytes);
599-
// C ABI markers.
600-
if text.contains("export_name") {
601-
c_override = first_quoted(text).map(str::to_owned);
602-
} else if text.contains("no_mangle") {
603-
c_no_mangle = true;
604-
}
605-
// Python (PyO3) markers — independent of the C markers above.
606-
if text.contains("pyfunction") {
607-
py = true;
608-
if let Some(v) = first_quoted(text) {
609-
py_override = Some(v.to_owned()); // `#[pyfunction(name = "…")]`
610-
}
611-
} else if text.contains("pyo3") {
612-
if let Some(v) = first_quoted(text) {
613-
py_override = Some(v.to_owned()); // `#[pyo3(name = "…")]`
614-
}
615-
}
616-
// WebAssembly/JS (wasm-bindgen) marker — `#[wasm_bindgen(js_name = "…")]`
617-
// overrides the JS-facing name.
618-
if text.contains("wasm_bindgen") {
619-
wasm = true;
620-
if let Some(v) = first_quoted(text) {
621-
wasm_override = Some(v.to_owned());
622-
}
623-
}
624-
// Node.js native addon (napi-rs) marker — `#[napi(js_name = "…")]`
625-
// overrides the JS-facing name.
626-
if text.contains("napi") {
627-
napi = true;
628-
if let Some(v) = first_quoted(text) {
629-
napi_override = Some(v.to_owned());
630-
}
631-
}
591+
attr_texts.push(node_text(&node, bytes));
632592
sib = node.prev_sibling();
633593
}
634-
635-
// A `#[no_mangle]`/`#[export_name]` symbol whose name follows the JNI
636-
// mangling convention is a Java Native Interface implementation, not a plain
637-
// C export — classify it so it bridges to Java, not C, call sites.
638-
let c_abi_for = |name: &str| {
639-
if name.starts_with("Java_") {
640-
FfiAbi::Jni
641-
} else {
642-
FfiAbi::C
643-
}
644-
};
645-
let mut out = Vec::new();
646-
if let Some(name) = c_override {
647-
out.push((c_abi_for(&name), name));
648-
} else if c_no_mangle {
649-
out.push((c_abi_for(fn_name), fn_name.to_owned()));
650-
}
651-
if py {
652-
out.push((
653-
FfiAbi::Python,
654-
py_override.unwrap_or_else(|| fn_name.to_owned()),
655-
));
656-
}
657-
if wasm {
658-
out.push((
659-
FfiAbi::Wasm,
660-
wasm_override.unwrap_or_else(|| fn_name.to_owned()),
661-
));
662-
}
663-
if napi {
664-
out.push((
665-
FfiAbi::NodeApi,
666-
napi_override.unwrap_or_else(|| fn_name.to_owned()),
667-
));
668-
}
669-
out
670-
}
671-
672-
/// The contents of the first double-quoted span in `s`, if any.
673-
fn first_quoted(s: &str) -> Option<&str> {
674-
let start = s.find('"')? + 1;
675-
let rest = &s[start..];
676-
let end = rest.find('"')?;
677-
Some(&rest[..end])
594+
crate::ffi::rust_exports(&attr_texts, fn_name)
678595
}
679596

680597
/// Display name for an `impl` block: the last type identifier before the body.

src/ffi/c.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! C ABI spec. `#[no_mangle]` exports under the function name; `#[export_name =
3+
//! "…"]` (with or without `#[no_mangle]`) overrides it — both mark a C export.
4+
use crate::graph::types::FfiAbi;
5+
6+
pub(crate) const SPEC: super::spec::AbiSpec = super::spec::AbiSpec {
7+
abi: FfiAbi::C,
8+
consumers: &["c", "cpp"],
9+
rust_attr_markers: &["no_mangle", "export_name"],
10+
rust_name_override_markers: &["export_name"],
11+
name_prefix: None,
12+
};

src/ffi/jni.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! Java Native Interface ABI spec. Produced by name, not by a Rust attribute:
3+
//! any export whose final name follows the `Java_<pkg>_<Class>_<method>`
4+
//! mangling is re-classified to JNI so it bridges to Java, not C, call sites.
5+
use crate::graph::types::FfiAbi;
6+
7+
pub(crate) const SPEC: super::spec::AbiSpec = super::spec::AbiSpec {
8+
abi: FfiAbi::Jni,
9+
consumers: &["java"],
10+
rust_attr_markers: &[],
11+
rust_name_override_markers: &[],
12+
name_prefix: Some("Java_"),
13+
};

src/ffi/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! Neutral per-ABI FFI registry: one self-contained spec file per `FfiAbi`.
3+
//! Both the producer extractors (marker→ABI classification) and the bridge
4+
//! resolver (consumer matrix) read from here, so adding an ABI is one spec file
5+
//! plus one `SPECS` entry — never a growing match or an inline extractor block.
6+
mod c;
7+
mod jni;
8+
mod node_api;
9+
mod python;
10+
mod spec;
11+
mod wasm;
12+
13+
pub(crate) use spec::{c_name_export_abi, consumers, rust_exports};

src/ffi/node_api.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! Node.js native addon (napi-rs) ABI spec. `#[napi]` exports under the function
3+
//! name; `#[napi(js_name = "…")]` overrides the JS-facing name.
4+
use crate::graph::types::FfiAbi;
5+
6+
pub(crate) const SPEC: super::spec::AbiSpec = super::spec::AbiSpec {
7+
abi: FfiAbi::NodeApi,
8+
consumers: &["javascript", "typescript"],
9+
rust_attr_markers: &["napi"],
10+
rust_name_override_markers: &["napi"],
11+
name_prefix: None,
12+
};

src/ffi/python.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! Python (PyO3) ABI spec. `#[pyfunction]` exports under the function name; a
3+
//! `#[pyfunction(name = "…")]` or `#[pyo3(name = "…")]` attribute overrides it.
4+
use crate::graph::types::FfiAbi;
5+
6+
pub(crate) const SPEC: super::spec::AbiSpec = super::spec::AbiSpec {
7+
abi: FfiAbi::Python,
8+
consumers: &["python"],
9+
rust_attr_markers: &["pyfunction"],
10+
rust_name_override_markers: &["pyfunction", "pyo3"],
11+
name_prefix: None,
12+
};

src/ffi/spec.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! `AbiSpec` registry and the lookups the producers/consumer use.
3+
use crate::graph::types::FfiAbi;
4+
5+
/// Everything per-ABI in one place. Add an ABI = add a sibling file with one of
6+
/// these consts + a line in `SPECS`.
7+
pub(crate) struct AbiSpec {
8+
pub abi: FfiAbi,
9+
/// Language tags (`Language::as_str`) whose call sites may consume this ABI.
10+
pub consumers: &'static [&'static str],
11+
/// Rust attribute substrings that MARK a fn as exported under this ABI
12+
/// (substring match against each `attribute_item`'s text). Empty for ABIs
13+
/// not produced directly from a Rust attribute (e.g. JNI, which arrives via
14+
/// the `name_prefix` rule).
15+
pub rust_attr_markers: &'static [&'static str],
16+
/// Rust attribute substrings that carry a quoted export-name override.
17+
pub rust_name_override_markers: &'static [&'static str],
18+
/// If set, ANY export whose final name starts with this prefix is
19+
/// re-classified to THIS abi (the `Java_` → `Jni` rule).
20+
pub name_prefix: Option<&'static str>,
21+
}
22+
23+
pub(crate) const SPECS: &[AbiSpec] = &[
24+
super::c::SPEC,
25+
super::python::SPEC,
26+
super::wasm::SPEC,
27+
super::node_api::SPEC,
28+
super::jni::SPEC,
29+
];
30+
31+
/// Consumer matrix lookup (replaces `FfiAbi::consumers`).
32+
pub(crate) fn consumers(abi: FfiAbi) -> &'static [&'static str] {
33+
SPECS
34+
.iter()
35+
.find(|s| s.abi == abi)
36+
.map_or(&[], |s| s.consumers)
37+
}
38+
39+
/// Final-name re-classification (e.g. `Java_*` → `Jni`). Returns `base` if no
40+
/// prefix rule matches.
41+
fn reclassify_by_name(base: FfiAbi, name: &str) -> FfiAbi {
42+
c_name_export_abi(name).unwrap_or(base)
43+
}
44+
45+
/// C-side by-name export classification (the `c.rs` `Java_` filter generalized).
46+
pub(crate) fn c_name_export_abi(name: &str) -> Option<FfiAbi> {
47+
SPECS
48+
.iter()
49+
.find(|s| s.name_prefix.is_some_and(|p| name.starts_with(p)))
50+
.map(|s| s.abi)
51+
}
52+
53+
/// Classify a Rust fn's FFI exports from its accumulated attribute texts.
54+
/// Returns `(abi, export_name)` pairs in `SPECS` order — multi-ABI capable,
55+
/// reproducing the prior inline classifier exactly.
56+
pub(crate) fn rust_exports(attr_texts: &[&str], fn_name: &str) -> Vec<(FfiAbi, String)> {
57+
let mut out = Vec::new();
58+
for spec in SPECS {
59+
if spec.rust_attr_markers.is_empty() {
60+
continue;
61+
}
62+
let enabled = attr_texts
63+
.iter()
64+
.any(|t| spec.rust_attr_markers.iter().any(|m| t.contains(m)));
65+
if !enabled {
66+
continue;
67+
}
68+
// The producer walks attributes bottom-up; the prior inline classifier
69+
// overwrote the override on each match, so the LAST matching attribute in
70+
// walk order wins. `.rev().find_map(...)` reproduces that precisely.
71+
let name = attr_texts
72+
.iter()
73+
.rev()
74+
.filter(|t| {
75+
spec.rust_name_override_markers
76+
.iter()
77+
.any(|m| t.contains(m))
78+
})
79+
.find_map(|t| first_quoted(t))
80+
.map(str::to_owned)
81+
.unwrap_or_else(|| fn_name.to_owned());
82+
out.push((reclassify_by_name(spec.abi, &name), name));
83+
}
84+
out
85+
}
86+
87+
/// The contents of the first double-quoted span in `s`, if any.
88+
fn first_quoted(s: &str) -> Option<&str> {
89+
let start = s.find('"')? + 1;
90+
let rest = &s[start..];
91+
let end = rest.find('"')?;
92+
Some(&rest[..end])
93+
}

src/ffi/wasm.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
//! WebAssembly/JS (wasm-bindgen) ABI spec. `#[wasm_bindgen]` exports under the
3+
//! function name; `#[wasm_bindgen(js_name = "…")]` overrides the JS-facing name.
4+
use crate::graph::types::FfiAbi;
5+
6+
pub(crate) const SPEC: super::spec::AbiSpec = super::spec::AbiSpec {
7+
abi: FfiAbi::Wasm,
8+
consumers: &["javascript", "typescript"],
9+
rust_attr_markers: &["wasm_bindgen"],
10+
rust_name_override_markers: &["wasm_bindgen"],
11+
name_prefix: None,
12+
};

src/graph/types.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -396,21 +396,6 @@ pub enum FfiAbi {
396396
Jni,
397397
}
398398

399-
impl FfiAbi {
400-
/// The language tags ([`Language::as_str`](crate::lang::Language::as_str))
401-
/// whose call sites can consume an export under this ABI. A bridge is only
402-
/// drawn to a consumer in one of these languages — so a C call never binds
403-
/// to a Python-only export, and vice versa.
404-
pub fn consumers(&self) -> &'static [&'static str] {
405-
match self {
406-
FfiAbi::C => &["c", "cpp"],
407-
FfiAbi::Python => &["python"],
408-
FfiAbi::Wasm | FfiAbi::NodeApi => &["javascript", "typescript"],
409-
FfiAbi::Jni => &["java"],
410-
}
411-
}
412-
}
413-
414399
/// A neutral cross-language export fact: the definition identified by [`symbol`]
415400
/// is callable from another language under [`export_name`] via [`abi`]. The
416401
/// extractor records it from a deterministic syntactic marker (e.g. Rust's

0 commit comments

Comments
 (0)