Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/public-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,8 @@ pub fn ea_compiler::bind_common::is_pointer(ty: &str) -> bool
pub fn ea_compiler::bind_common::parse_exports(json: &str) -> core::result::Result<alloc::vec::Vec<ea_compiler::bind_common::ExportFunc>, alloc::string::String>
pub fn ea_compiler::bind_common::parse_string_field(json: &str, key: &str) -> core::option::Option<alloc::string::String>
pub fn ea_compiler::bind_common::pointer_inner(ty: &str) -> core::option::Option<&str>
pub fn ea_compiler::bind_common::python_lib_path_expr(lib_name: &str) -> alloc::string::String
pub fn ea_compiler::bind_common::rust_link_stem(lib_name: &str) -> alloc::string::String
pub mod ea_compiler::bind_cpp
pub fn ea_compiler::bind_cpp::generate(json_str: &str, module_stem: &str) -> core::result::Result<alloc::string::String, alloc::string::String>
pub mod ea_compiler::bind_python
Expand Down
24 changes: 24 additions & 0 deletions src/bind_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,27 @@ fn find_matching_brace(s: &str) -> Result<usize, String> {
}
Err("unmatched '{'".into())
}

/// Build a Python expression that resolves `lib_name` relative to the generated
/// module's directory. Splits on `/` so directory components survive
/// `pathlib.Path.with_name`'s no-slashes constraint (issue #1).
pub fn python_lib_path_expr(lib_name: &str) -> String {
let mut expr = String::from("_Path(__file__).parent");
for segment in lib_name.split('/').filter(|s| !s.is_empty()) {
expr.push_str(" / \"");
expr.push_str(segment);
expr.push('"');
}
expr
}

/// Normalize a library path/filename into a Rust `#[link(name = ...)]` stem.
/// Strips directory components, the trailing `.so`/`.dll`, and the Unix `lib`
/// prefix — e.g. `lib/libfoo.so` -> `foo` (issue #1).
pub fn rust_link_stem(lib_name: &str) -> String {
let basename = lib_name.rsplit('/').next().unwrap_or(lib_name);
let trimmed = basename
.trim_end_matches(".so")
.trim_end_matches(".dll");
trimmed.strip_prefix("lib").unwrap_or(trimmed).to_string()
}
5 changes: 3 additions & 2 deletions src/bind_python.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::bind_common::{
ExportFunc, find_collapsed_args, has_out_params, parse_exports, parse_string_field,
pointer_inner,
pointer_inner, python_lib_path_expr,
};

pub fn generate(json_str: &str, module_stem: &str) -> Result<String, String> {
Expand All @@ -16,7 +16,8 @@ pub fn generate(json_str: &str, module_stem: &str) -> Result<String, String> {
out.push_str("import numpy as _np\n");
out.push_str("from pathlib import Path as _Path\n\n");
out.push_str(&format!(
"_lib = _ct.CDLL(str(_Path(__file__).with_name(\"{lib_name}\")))\n\n"
"_lib = _ct.CDLL(str({}))\n\n",
python_lib_path_expr(&lib_name)
));

let has_parallel = exports.iter().any(crate::bind_common::is_parallelizable);
Expand Down
5 changes: 3 additions & 2 deletions src/bind_pytorch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::bind_common::{
ExportFunc, find_collapsed_args, is_mut_pointer, parse_exports, parse_string_field,
pointer_inner,
pointer_inner, python_lib_path_expr,
};

pub fn generate(json_str: &str, module_stem: &str) -> Result<String, String> {
Expand All @@ -16,7 +16,8 @@ pub fn generate(json_str: &str, module_stem: &str) -> Result<String, String> {
out.push_str("import torch as _torch\n");
out.push_str("from pathlib import Path as _Path\n\n");
out.push_str(&format!(
"_lib = _ct.CDLL(str(_Path(__file__).with_name(\"{lib_name}\")))\n"
"_lib = _ct.CDLL(str({}))\n",
python_lib_path_expr(&lib_name)
));

for func in &exports {
Expand Down
8 changes: 2 additions & 6 deletions src/bind_rust.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
use crate::bind_common::{
ExportFunc, find_collapsed_args, is_mut_pointer, parse_exports, parse_string_field,
pointer_inner,
pointer_inner, rust_link_stem,
};

pub fn generate(json_str: &str, module_stem: &str) -> Result<String, String> {
let exports = parse_exports(json_str)?;
let lib_name = parse_string_field(json_str, "library")
.map(|l| {
l.trim_end_matches(".so")
.trim_end_matches(".dll")
.to_string()
})
.map(|l| rust_link_stem(&l))
.unwrap_or_else(|| module_stem.to_string());

let mut out = String::new();
Expand Down
30 changes: 30 additions & 0 deletions tests/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,36 @@ fn test_python_parallel_in_all() {
);
}

#[test]
fn test_python_library_path_with_slash() {
// Issue #1: when `library` contains a directory component (e.g. `-o lib/libfoo.so`),
// generated Python used `Path.with_name("lib/libfoo.so")` which raises ValueError —
// `with_name` rejects any name containing `/`. Fix splits the path and joins it
// onto `_Path(__file__).parent`.
let json = r#"{"library": "lib/libfoo.so", "exports": [{"name": "noop", "args": [], "return_type": null}], "structs": []}"#;
let py = ea_compiler::bind_python::generate(json, "foo").unwrap();

assert!(
!py.contains("with_name(\"lib/libfoo.so\")"),
"must not pass slashed path to with_name (Python rejects it), got:\n{py}"
);
assert!(
py.contains("_Path(__file__).parent / \"lib\" / \"libfoo.so\""),
"should build path by joining parent with each segment, got:\n{py}"
);
}

#[test]
fn test_python_library_plain_name_unchanged() {
// Plain (no-slash) library names should still work — emitted as a single parent/name join.
let json = r#"{"library": "kernel.so", "exports": [{"name": "noop", "args": [], "return_type": null}], "structs": []}"#;
let py = ea_compiler::bind_python::generate(json, "kernel").unwrap();
assert!(
py.contains("_Path(__file__).parent / \"kernel.so\""),
"should join parent with library name, got:\n{py}"
);
}

#[test]
fn test_is_not_parallelizable_no_pointer() {
use ea_compiler::bind_common::{Arg, ExportFunc, is_parallelizable};
Expand Down
38 changes: 38 additions & 0 deletions tests/bind_generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,44 @@ fn test_pytorch_type_hints() {
);
}

#[test]
fn test_pytorch_library_path_with_slash() {
// Issue #1: same `with_name` bug as bind_python — pytorch binding must split path segments.
let json = r#"{"library": "lib/libfoo.so", "exports": [{"name": "noop", "args": [], "return_type": null}], "structs": []}"#;
let py = ea_compiler::bind_pytorch::generate(json, "foo").unwrap();

assert!(
!py.contains("with_name(\"lib/libfoo.so\")"),
"must not pass slashed path to with_name, got:\n{py}"
);
assert!(
py.contains("_Path(__file__).parent / \"lib\" / \"libfoo.so\""),
"should build path by joining parent with each segment, got:\n{py}"
);
}

#[test]
fn test_rust_library_path_with_slash() {
// Issue #1 (Rust variant): `#[link(name = "lib/libfoo")]` is invalid — Rust link
// names must be a bare library stem (no path, no `lib` prefix on Unix).
// For `library: "lib/libfoo.so"`, the link name should be `foo`.
let json = r#"{"library": "lib/libfoo.so", "exports": [{"name": "noop", "args": [], "return_type": null}], "structs": []}"#;
let rs = ea_compiler::bind_rust::generate(json, "foo").unwrap();

assert!(
!rs.contains("name = \"lib/libfoo\""),
"link name must not contain directory components, got:\n{rs}"
);
assert!(
!rs.contains("name = \"libfoo\""),
"link name must strip the `lib` prefix (Unix convention), got:\n{rs}"
);
assert!(
rs.contains("#[link(name = \"foo\")]"),
"expected stem-only link name `foo`, got:\n{rs}"
);
}

#[test]
fn test_pytorch_friendly_docstring() {
let json = r#"{"library": "k.so", "exports": [{"name": "dot", "args": [{"name": "a", "type": "*f32"}, {"name": "b", "type": "*f32"}, {"name": "n", "type": "i32"}], "return_type": "f32"}], "structs": []}"#;
Expand Down
Loading