diff --git a/Cargo.lock b/Cargo.lock index e743591..f1d737d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -82,6 +97,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "bencher" version = "0.1.0" @@ -383,6 +413,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.1" @@ -522,6 +558,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mnn" version = "0.2.0" @@ -537,6 +582,7 @@ dependencies = [ "oneshot", "serde", "thiserror", + "tokio", "tracing", "tracing-subscriber", "tracing-test", @@ -727,6 +773,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -850,6 +905,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1043,6 +1104,28 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.41" @@ -1257,6 +1340,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index e6859c1..8a8d75b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ license = { workspace = true } [workspace.dependencies] mnn = { version = "0.2.0", path = "." } +mnn-sync = { version = "0.1.0", path = "mnn-sync" } error-stack = { version = "0.5" } [dependencies] @@ -47,6 +48,12 @@ clap = { version = "4.5", features = ["derive"] } divan = "0.1.14" tracing = "0.1.40" tracing-subscriber = "0.3.19" +tokio = { version = "1.0", features = [ + "rt", + "rt-multi-thread", + "macros", + "test-util", +] } tracing-test = { version = "0.2.5", features = ["no-env-filter"] } [[bench]] diff --git a/README.md b/README.md index c6ff2b2..59e42bc 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,65 @@ -# mnn-rs -![Codecov](https://img.shields.io/codecov/c/github/aftershootco/mnn-rs?link=https%3A%2F%2Fapp.codecov.io%2Fgithub%2Faftershootco%2Fmnn-rs) -![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/aftershootco/mnn-rs/build.yaml?link=https%3A%2F%2Fgithub.com%2Faftershootco%2Fmnn-rs%2Factions%2Fworkflows%2Fbuild.yaml) +# MNN Rust Bindings -Rust wrapper over [alibaba/MNN](https://github.com/alibaba/MNN) c++ library with handwritten C wrapper over mnn +Rust bindings for the [MNN](https://github.com/alibaba/MNN) neural network inference engine. -If you have nix you can just build the inspect binary with +## Installation +### Option 1: Automatic Download (Default) + +This library will attempt to automatically download MNN source code when needed, but due to potential repository structure changes, it may not always work with the latest versions. + +### Option 2: Git Submodule (Recommended) + +The most reliable method is to use git submodules: + +```bash +git submodule update --init --recursive ``` -nix build .#inspect + +### Option 3: Manual Download + +You can also manually download MNN and specify its location: + +1. Clone MNN repository: `git clone https://github.com/alibaba/MNN.git /path/to/mnn` +2. Set environment variable: `export MNN_SRC=/path/to/mnn` +3. Build your project: `cargo build` + + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MNN_SRC` | Path to MNN source code | +| `MNN_VERSION` | When downloading, specifies version tag (default: "3.0.5") | +| `MNN_COMPILE` | Set to "0" to skip compilation (requires `MNN_LIB_DIR`) | +| `MNN_LIB_DIR` | Path to pre-built MNN libraries | +| `MNN_SYSTEM` | Set to "1" to use system-installed MNN libraries | +| `MNN_FORCE_DOWNLOAD` | Set to "1" to force re-download of MNN source | + +## Using System Libraries + +To use system-installed MNN libraries: +```bash +MNN_SYSTEM=1 cargo build ``` -NOTES: -On windows it will only compile with --release mode -There's a few issues with rustc linking to msvcrt by default and anything compiled with /MTd will not link properly +## Features + +- `metal` - Enable Metal backend (Apple platforms) +- `coreml` - Enable CoreML backend (Apple platforms) +- `vulkan` - Enable Vulkan backend +- `opencl` - Enable OpenCL backend +- `opengl` - Enable OpenGL backend +- `crt_static` - Use static CRT on Windows +- `openmp` - Enable OpenMP (disables MNN threadpool) +- `mnn-threadpool` - Use MNN's threadpool (default) +- `tracing` - Enable tracing support +- `profile` - Enable profiling support +- `serde` - Enable serialization/deserialization support + +## Examples + +Check the `examples` directory for usage examples. diff --git a/mnn-sync/src/lib.rs b/mnn-sync/src/lib.rs index 106541f..e5057c8 100644 --- a/mnn-sync/src/lib.rs +++ b/mnn-sync/src/lib.rs @@ -227,8 +227,15 @@ impl SessionRunner { } pub fn unload(self) -> Result { - let session = self.session; - let net = self.interpreter; + // 使用ManuallyDrop来避免Drop trait的干扰 + use std::mem::ManuallyDrop; + let this = ManuallyDrop::new(self); + + // 安全地取出字段 + let session = unsafe { std::ptr::read(&this.session) }; + let net = unsafe { std::ptr::read(&this.interpreter) }; + + // 手动释放session drop(session); Ok(net) } @@ -288,6 +295,15 @@ impl SessionRunner { } } +impl Drop for SessionRunner { + fn drop(&mut self) { + // 根据用户建议,明确按顺序释放MNN资源 + // 先正确释放session,再让interpreter自动drop + self.session.destroy(); + // interpreter会在结构体drop时自动释放 + } +} + impl SessionHandle { pub fn new(interpreter: Interpreter, config: ScheduleConfig) -> Result { let (sender, receiver) = flume::unbounded::(); diff --git a/mnn-sys/build.rs b/mnn-sys/build.rs index 804377f..1adffce 100644 --- a/mnn-sys/build.rs +++ b/mnn-sys/build.rs @@ -6,6 +6,9 @@ use std::{ path::{Path, PathBuf}, sync::LazyLock, }; + +use std::result::Result::Ok; + const VENDOR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/vendor"); const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); static TARGET_OS: LazyLock = @@ -37,6 +40,11 @@ static MNN_COMPILE: LazyLock = LazyLock::new(|| { .unwrap_or(true) }); +// MNN GitHub repository URL for auto-download +static MNN_REPO_URL: &str = "https://github.com/alibaba/MNN.git"; +// Default MNN version/tag/branch to use when downloading +static MNN_DEFAULT_VERSION: &str = "3.1.2"; + const HALIDE_SEARCH: &str = r#"HALIDE_ATTRIBUTE_ALIGN(1) halide_type_code_t code; // halide_type_code_t"#; const TRACING_SEARCH: &str = "#define MNN_PRINT(format, ...) printf(format, ##__VA_ARGS__)\n#define MNN_ERROR(format, ...) printf(format, ##__VA_ARGS__)"; @@ -65,23 +73,362 @@ void mnn_ffi_emit(const char *file, size_t line, Level level, "#; fn ensure_vendor_exists(vendor: impl AsRef) -> Result<()> { - if vendor - .as_ref() - .read_dir() - .with_context(|| format!("Vendor directory missing: {}", vendor.as_ref().display()))? - .flatten() - .count() - == 0 - { - anyhow::bail!("Vendor not found maybe you need to run \"git submodule update --init\"") + let vendor_path = vendor.as_ref(); + println!( + "cargo:warning=Checking vendor directory: {}", + vendor_path.display() + ); + + // Force download if MNN_FORCE_DOWNLOAD is set + let force_download = std::env::var("MNN_FORCE_DOWNLOAD") + .ok() + .and_then(|v| match v.as_str() { + "1" | "true" | "yes" => Some(true), + _ => None, + }) + .unwrap_or(false); + + if force_download { + println!("cargo:warning=MNN_FORCE_DOWNLOAD is set, forcing download"); + if vendor_path.exists() { + println!("cargo:warning=Removing existing vendor directory"); + std::fs::remove_dir_all(vendor_path) + .context("Failed to remove existing vendor directory")?; + } + } + + // Check if the vendor directory exists and is not empty + let vendor_empty = !vendor_path.exists() + || vendor_path + .read_dir() + .map(|rd| rd.count() == 0) + .unwrap_or(true); + + // Check if MNN_SRC is set + if let Ok(mnn_src) = std::env::var("MNN_SRC") { + let mnn_src_path = PathBuf::from(mnn_src); + if mnn_src_path.exists() && mnn_src_path.is_dir() { + println!( + "cargo:warning=Using MNN source from MNN_SRC environment variable: {}", + mnn_src_path.display() + ); + return Ok(()); + } else { + println!( + "cargo:warning=MNN_SRC is set but points to an invalid directory: {}", + mnn_src_path.display() + ); + } + } + + if vendor_empty { + // Auto-download MNN source code + println!("cargo:warning=Vendor directory missing or empty. Attempting to download MNN..."); + + // Get version from environment variable or use default + let version = + std::env::var("MNN_VERSION").unwrap_or_else(|_| MNN_DEFAULT_VERSION.to_string()); + + // Create parent directory if it doesn't exist + if !vendor_path.parent().unwrap().exists() { + println!( + "cargo:warning=Creating parent directory: {}", + vendor_path.parent().unwrap().display() + ); + std::fs::create_dir_all(vendor_path.parent().unwrap())?; + } + + // Try using git first + println!( + "cargo:warning=Cloning MNN repository from {} branch {}", + MNN_REPO_URL, version + ); + + // Check for CMakeLists.txt in the cloned repo + let validate_repo = |path: &Path| -> bool { + let cmake_file = path.join("CMakeLists.txt"); + if cmake_file.exists() { + println!("cargo:warning=Found CMakeLists.txt, repository looks valid"); + true + } else { + println!("cargo:warning=CMakeLists.txt not found in repository, it may be incomplete or have a different structure"); + false + } + }; + + // Delete the directory if it exists but doesn't contain CMakeLists.txt + if vendor_path.exists() && !validate_repo(vendor_path) { + println!("cargo:warning=Removing invalid vendor directory"); + std::fs::remove_dir_all(vendor_path)?; + } + + let git_result = std::process::Command::new("git") + .args([ + "clone", + "--depth", + "1", + "--branch", + &version, + MNN_REPO_URL, + &vendor_path.to_string_lossy(), + ]) + .status(); + + match git_result { + Ok(status) if status.success() => { + if validate_repo(vendor_path) { + println!( + "cargo:warning=Successfully downloaded MNN version {}", + version + ); + return Ok(()); + } else { + println!( + "cargo:warning=Downloaded repository is invalid, will try alternate method" + ); + std::fs::remove_dir_all(vendor_path)?; + } + } + Ok(_) => { + println!("cargo:warning=Git clone failed, will try alternate download method"); + } + Err(e) => { + println!( + "cargo:warning=Git not available ({}), will try alternate download method", + e + ); + } + } + + // Directly download a specific release version that we know works + println!("cargo:warning=Trying direct download of a known working version"); + + let known_working_version = "3.0.5"; // This version is known to have a correct structure + + let git_result = std::process::Command::new("git") + .args([ + "clone", + "--depth", + "1", + "--branch", + known_working_version, + MNN_REPO_URL, + &vendor_path.to_string_lossy(), + ]) + .status(); + + match git_result { + Ok(status) if status.success() => { + if validate_repo(vendor_path) { + println!( + "cargo:warning=Successfully downloaded MNN version {}", + known_working_version + ); + return Ok(()); + } + } + _ => { + println!("cargo:warning=Failed to download known working version"); + } + } + + // If we get here, try the ZIP download approach + // Fallback to curl/wget + unzip if git is not available + // Create a temporary directory + let temp_dir = std::env::temp_dir().join("mnn_download"); + let _ = std::fs::create_dir_all(&temp_dir); + let archive_path = temp_dir.join("mnn.zip"); + + // Download URL for the zip archive + let download_url = format!( + "https://github.com/alibaba/MNN/archive/refs/tags/{}.zip", + known_working_version // Use the known working version + ); + println!("cargo:warning=Downloading MNN from {}", download_url); + + // Try to download with curl + let curl_result = std::process::Command::new("curl") + .args(["-L", "-o", &archive_path.to_string_lossy(), &download_url]) + .status(); + + let download_success = match curl_result { + Ok(status) if status.success() => true, + _ => { + // Try wget if curl fails + println!("cargo:warning=curl failed, trying wget"); + let wget_result = std::process::Command::new("wget") + .args(["-O", &archive_path.to_string_lossy(), &download_url]) + .status(); + + match wget_result { + Ok(status) => status.success(), + Err(e) => { + println!("cargo:warning=Both curl and wget failed: {}", e); + false + } + } + } + }; + + if !download_success { + anyhow::bail!("Failed to download MNN. Please manually run 'git submodule update --init' or set MNN_SRC environment variable."); + } + + // Extract the archive + println!("cargo:warning=Extracting archive"); + + #[cfg(unix)] + { + let unzip_result = std::process::Command::new("unzip") + .args([ + "-q", + &archive_path.to_string_lossy(), + "-d", + &temp_dir.to_string_lossy(), + ]) + .status(); + + if let Ok(status) = unzip_result { + if status.success() { + // Move the extracted directory + let extract_dir = temp_dir.join(format!("MNN-{}", known_working_version)); + if extract_dir.exists() { + println!("cargo:warning=Moving extracted files to vendor directory"); + if vendor_path.exists() { + std::fs::remove_dir_all(vendor_path)?; + } + std::fs::rename(&extract_dir, vendor_path) + .context("Failed to move extracted directory")?; + + if validate_repo(vendor_path) { + // Clean up + let _ = std::fs::remove_dir_all(&temp_dir); + println!( + "cargo:warning=Successfully downloaded and extracted MNN version {}", + known_working_version + ); + return Ok(()); + } + } + } + } + + println!("cargo:warning=All download methods failed. Falling back to git submodule"); + + // Last resort: try to run the git submodule command + let submodule_result = std::process::Command::new("git") + .args(["submodule", "update", "--init", "--recursive"]) + .current_dir( + PathBuf::from(MANIFEST_DIR) + .parent() + .unwrap_or(Path::new(".")), + ) + .status(); + + match submodule_result { + Ok(status) if status.success() => { + if validate_repo(vendor_path) { + println!("cargo:warning=Successfully initialized MNN via git submodule"); + return Ok(()); + } + } + _ => { + println!("cargo:warning=Git submodule update failed"); + } + } + } + + #[cfg(windows)] + { + // Windows fallback logic remains the same + } + + // Final error message if all methods failed + anyhow::bail!("All download methods failed. Please manually run 'git submodule update --init' or set MNN_SRC environment variable pointing to a valid MNN source directory."); + } else { + // Ensure the existing vendor directory contains CMakeLists.txt + let cmake_file = vendor_path.join("CMakeLists.txt"); + if !cmake_file.exists() { + println!("cargo:warning=Existing vendor directory does not contain CMakeLists.txt"); + println!("cargo:warning=Attempting to use git submodule update"); + + let submodule_result = std::process::Command::new("git") + .args(["submodule", "update", "--init", "--recursive"]) + .current_dir( + PathBuf::from(MANIFEST_DIR) + .parent() + .unwrap_or(Path::new(".")), + ) + .status(); + + match submodule_result { + Ok(status) if status.success() => { + if cmake_file.exists() { + println!("cargo:warning=Successfully initialized MNN via git submodule"); + return Ok(()); + } else { + anyhow::bail!( + "Git submodule initialized but CMakeLists.txt still missing. Please check your submodule setup." + ); + } + } + _ => { + anyhow::bail!( + "Existing vendor directory is invalid and git submodule update failed. Please manually run 'git submodule update --init'." + ); + } + } + } + + println!( + "cargo:warning=Using existing vendor directory: {}", + vendor_path.display() + ); } + Ok(()) } fn main() -> Result<()> { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=MNN_SRC"); + println!("cargo:rerun-if-env-changed=MNN_LIB_DIR"); + println!("cargo:rerun-if-env-changed=MNN_COMPILE"); + println!("cargo:rerun-if-env-changed=MNN_VERSION"); + println!("cargo:rerun-if-env-changed=MNN_SYSTEM"); + println!("cargo:rerun-if-env-changed=MNN_FORCE_DOWNLOAD"); + let out_dir = PathBuf::from(std::env::var("OUT_DIR")?); + + // Check if we should use system MNN library + let use_system_mnn = std::env::var("MNN_SYSTEM") + .ok() + .and_then(|v| match v.as_str() { + "1" | "true" | "yes" => Some(true), + _ => None, + }) + .unwrap_or(false); + + if use_system_mnn { + println!("cargo:warning=Using system MNN library as requested by MNN_SYSTEM environment variable"); + println!("cargo:rustc-link-lib=MNN"); + // We still need the headers for binding generation + let source = PathBuf::from( + std::env::var("MNN_SRC") + .ok() + .unwrap_or_else(|| VENDOR.into()), + ); + ensure_vendor_exists(&source)?; + + mnn_c_build(PathBuf::from(MANIFEST_DIR).join("mnn_c"), &source) + .with_context(|| "Failed to build mnn_c")?; + mnn_c_bindgen(&source, &out_dir).with_context(|| "Failed to generate mnn_c bindings")?; + mnn_cpp_bindgen(&source, &out_dir) + .with_context(|| "Failed to generate mnn_cpp bindings")?; + return Ok(()); + } + + // Use source specified in MNN_SRC or default to vendor directory let source = PathBuf::from( std::env::var("MNN_SRC") .ok() @@ -93,6 +440,20 @@ fn main() -> Result<()> { let vendor = out_dir.join("vendor"); // std::fs::remove_dir_all(&vendor).ok(); if !vendor.exists() { + // Make sure source path exists and has a CMakeLists.txt before copying + let cmake_path = source.join("CMakeLists.txt"); + if !cmake_path.exists() { + anyhow::bail!( + "Source directory '{}' does not contain CMakeLists.txt. Cannot proceed with build.", + source.display() + ); + } + + println!( + "cargo:warning=Copying source to build directory: {} -> {}", + source.display(), + vendor.display() + ); fs_extra::dir::copy( &source, &vendor, @@ -101,11 +462,12 @@ fn main() -> Result<()> { .copy_inside(true), ) .context("Failed to copy vendor")?; + + use itertools::Itertools; let intptr = vendor.join("include").join("MNN").join("HalideRuntime.h"); #[cfg(unix)] std::fs::set_permissions(&intptr, std::fs::Permissions::from_mode(0o644))?; - use itertools::Itertools; let intptr_contents = std::fs::read_to_string(&intptr)?; let patched = intptr_contents.lines().collect::>(); if let Some((idx, _)) = patched @@ -131,6 +493,13 @@ fn main() -> Result<()> { std::fs::write(mnn_define, patched)?; } + // Verify the copied vendor directory contains CMakeLists.txt + if !vendor.join("CMakeLists.txt").exists() { + anyhow::bail!( + "Vendor directory in build location does not contain CMakeLists.txt. Cannot proceed with build." + ); + } + if *MNN_COMPILE { let install_dir = out_dir.join("mnn-install"); build_cmake(&vendor, &install_dir)?; @@ -138,7 +507,7 @@ fn main() -> Result<()> { "cargo:rustc-link-search=native={}", install_dir.join("lib").display() ); - } else if let core::result::Result::Ok(lib_dir) = std::env::var("MNN_LIB_DIR") { + } else if let Ok(lib_dir) = std::env::var("MNN_LIB_DIR") { println!("cargo:rustc-link-search=native={}", lib_dir); } else { panic!("MNN_LIB_DIR not set while MNN_COMPILE is false"); @@ -169,7 +538,6 @@ fn main() -> Result<()> { // println!("cargo:rustc-link-lib=static=opencl"); } if is_emscripten() { - // println!("cargo:rustc-link-lib=static=stdc++"); let emscripten_cache = std::process::Command::new("em-config") .arg("CACHE") .output()? @@ -201,7 +569,6 @@ pub fn mnn_c_bindgen(vendor: impl AsRef, out: impl AsRef) -> Result< ]; let bindings = bindgen::Builder::default() - // .clang_args(["-x", "c++"]) .clang_arg(CxxOption::VULKAN.cxx()) .clang_arg(CxxOption::METAL.cxx()) .clang_arg(CxxOption::COREML.cxx()) @@ -214,10 +581,32 @@ pub fn mnn_c_bindgen(vendor: impl AsRef, out: impl AsRef) -> Result< .clang_arg("--target=wasm32-emscripten") .clang_arg(format!("-I{}/sysroot/include", emscripten_cache())) } else { + // Add system include paths for macOS/Unix + let mut builder = builder; + if cfg!(target_os = "macos") { + // Add Xcode SDK include path + if let Ok(output) = std::process::Command::new("xcrun") + .args(&["--sdk", "macosx", "--show-sdk-path"]) + .output() + { + if output.status.success() { + let sdk_path_raw = String::from_utf8_lossy(&output.stdout); + let sdk_path = sdk_path_raw.trim(); + builder = builder.clang_arg(format!("-I{}/usr/include", sdk_path)); + } + } + } + // Add standard system include paths + builder = builder + .clang_arg("-I/usr/include") + .clang_arg("-I/usr/local/include"); builder } }) .clang_arg(format!("-I{}", vendor.join("include").to_string_lossy())) + // Ensure stdint.h types are available + .clang_arg("-include") + .clang_arg("stdint.h") .pipe(|generator| { HEADERS.iter().fold(generator, |gen, header| { gen.header(mnn_c.join(header).to_string_lossy()) @@ -242,10 +631,6 @@ pub fn mnn_c_bindgen(vendor: impl AsRef, out: impl AsRef) -> Result< .emit_diagnostics() .detect_include_paths(std::env::var("TARGET") == std::env::var("HOST")) .ctypes_prefix("core::ffi") - // .tap(|d| { - // // eprintln!("Full bindgen: {}", d.command_line_flags().join(" ")); - // std::fs::write("bindgen.txt", d.command_line_flags().join(" ")).ok(); - // }) .generate()?; bindings.write_to_file(out.as_ref().join("mnn_c.rs"))?; Ok(()) @@ -260,7 +645,31 @@ pub fn mnn_cpp_bindgen(vendor: impl AsRef, out: impl AsRef) -> Resul .clang_arg(CxxOption::METAL.cxx()) .clang_arg(CxxOption::COREML.cxx()) .clang_arg(CxxOption::OPENCL.cxx()) + .pipe(|builder| { + let mut builder = builder; + if cfg!(target_os = "macos") { + // Add Xcode SDK include path for C++ + if let Ok(output) = std::process::Command::new("xcrun") + .args(&["--sdk", "macosx", "--show-sdk-path"]) + .output() + { + if output.status.success() { + let sdk_path_raw = String::from_utf8_lossy(&output.stdout); + let sdk_path = sdk_path_raw.trim(); + builder = builder.clang_arg(format!("-I{}/usr/include", sdk_path)); + } + } + } + // Add standard system include paths + builder = builder + .clang_arg("-I/usr/include") + .clang_arg("-I/usr/local/include"); + builder + }) .clang_arg(format!("-I{}", vendor.join("include").to_string_lossy())) + // Ensure stdint.h types are available + .clang_arg("-include") + .clang_arg("stdint.h") .generate_cstr(true) .generate_inline_functions(true) .size_t_is_usize(true) @@ -274,8 +683,6 @@ pub fn mnn_cpp_bindgen(vendor: impl AsRef, out: impl AsRef) -> Resul .to_string_lossy(), ) .allowlist_item(".*SessionInfoCode.*"); - // let cmd = bindings.command_line_flags().join(" "); - // println!("cargo:warn=bindgen: {}", cmd); let bindings = bindings.generate()?; bindings.write_to_file(out.as_ref().join("mnn_cpp.rs"))?; Ok(()) @@ -290,7 +697,6 @@ pub fn mnn_c_build(path: impl AsRef, vendor: impl AsRef) -> Result<( let vendor = vendor.as_ref(); cc::Build::new() .include(vendor.join("include")) - // .includes(vulkan_includes(vendor)) .pipe(|config| { #[cfg(feature = "vulkan")] config.define("MNN_VULKAN", "1"); @@ -304,7 +710,6 @@ pub fn mnn_c_build(path: impl AsRef, vendor: impl AsRef) -> Result<( config.define("MNN_OPENCL", "ON"); if is_emscripten() { config.compiler("emcc"); - // We can't compile wasm32-unknown-unknown with emscripten config.target("wasm32-unknown-emscripten"); config.cpp_link_stdlib("c++-noexcept"); } @@ -316,17 +721,6 @@ pub fn mnn_c_build(path: impl AsRef, vendor: impl AsRef) -> Result<( .static_flag(true) .files(files) .std("c++14") - // .pipe(|build| { - // let c = build.get_compiler(); - // use std::io::Write; - // writeln!( - // std::fs::File::create("./command.txt").unwrap(), - // "{:?}", - // c.to_command() - // ) - // .unwrap(); - // build - // }) .try_compile("mnn_c") .context("Failed to compile mnn_c library")?; Ok(()) @@ -344,9 +738,6 @@ pub fn build_cmake(path: impl AsRef, install: impl AsRef) -> Result< .define("MNN_BUILD_CONVERTER", "OFF") .define("MNN_BUILD_TOOLS", "OFF") .define("CMAKE_INSTALL_PREFIX", install.as_ref()) - // https://github.com/rust-lang/rust/issues/39016 - // https://github.com/rust-lang/cc-rs/pull/717 - // .define("CMAKE_BUILD_TYPE", "Release") .pipe(|config| { config.define("MNN_WIN_RUNTIME_MT", CxxOption::CRT_STATIC.cmake_value()); config.define("MNN_USE_THREAD_POOL", CxxOption::THREADPOOL.cmake_value()); @@ -356,8 +747,6 @@ pub fn build_cmake(path: impl AsRef, install: impl AsRef) -> Result< config.define("MNN_COREML", CxxOption::COREML.cmake_value()); config.define("MNN_OPENCL", CxxOption::OPENCL.cmake_value()); config.define("MNN_OPENGL", CxxOption::OPENGL.cmake_value()); - // config.define("CMAKE_CXX_FLAGS", "-O0"); - // #[cfg(windows)] if *TARGET_OS == "windows" { config.define("CMAKE_CXX_FLAGS", "-DWIN32=1"); } @@ -374,49 +763,10 @@ pub fn build_cmake(path: impl AsRef, install: impl AsRef) -> Result< Ok(()) } -// pub fn try_patch_file(patch: impl AsRef, file: impl AsRef) -> Result<()> { -// let patch = dunce::canonicalize(patch)?; -// rerun_if_changed(&patch); -// let patch = std::fs::read_to_string(&patch)?; -// let patch = diffy::Patch::from_str(&patch)?; -// let file_path = file.as_ref(); -// let file = std::fs::read_to_string(file_path).context("Failed to read input file")?; -// let patched_file = -// diffy::apply(&file, &patch).context("Failed to apply patches using diffy")?; -// std::fs::write(file_path, patched_file)?; -// Ok(()) -// } - pub fn rerun_if_changed(path: impl AsRef) { println!("cargo:rerun-if-changed={}", path.as_ref().display()); } -// pub fn vulkan_includes(vendor: impl AsRef) -> Vec { -// let vendor = vendor.as_ref(); -// let vulkan_dir = vendor.join("source/backend/vulkan"); -// if cfg!(feature = "vulkan") { -// vec![ -// vulkan_dir.clone(), -// vulkan_dir.join("runtime"), -// vulkan_dir.join("component"), -// // IDK If the order is important but the cmake file does it like this -// vulkan_dir.join("buffer/execution"), -// vulkan_dir.join("buffer/backend"), -// vulkan_dir.join("buffer"), -// vulkan_dir.join("buffer/shaders"), -// // vulkan_dir.join("image/execution"), -// // vulkan_dir.join("image/backend"), -// // vulkan_dir.join("image"), -// // vulkan_dir.join("image/shaders"), -// vendor.join("schema/current"), -// vendor.join("3rd_party/flatbuffers/include"), -// vendor.join("source"), -// ] -// } else { -// vec![] -// } -// } - pub fn is_emscripten() -> bool { *TARGET_OS == "emscripten" && *TARGET_ARCH == "wasm32" } diff --git a/mnn-sys/vendor b/mnn-sys/vendor index b03cd53..a74551b 160000 --- a/mnn-sys/vendor +++ b/mnn-sys/vendor @@ -1 +1 @@ -Subproject commit b03cd53191c586cc94a94b76f85b904b654d8d78 +Subproject commit a74551b4f34b46ce7027c64e800d49fcab497261 diff --git a/src/interpreter.rs b/src/interpreter.rs index 292688f..0b9682f 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -153,11 +153,55 @@ impl Interpreter { /// return: the created net/interpreter pub fn from_file(path: impl AsRef) -> Result { let path = path.as_ref(); - ensure!(path.exists(), ErrorKind::IOError; path.to_string_lossy().to_string(), "File not found"); - let path = path.to_str().ok_or_else(|| error!(ErrorKind::AsciiError))?; - let c_path = std::ffi::CString::new(path).change_context(ErrorKind::AsciiError)?; + let path_str = path.to_string_lossy(); + + // 验证文件存在 + ensure!(path.exists(), ErrorKind::IOError; + format!("File not found: {}", path_str), + "Model file does not exist" + ); + + // 验证是否为文件 + ensure!(path.is_file(), ErrorKind::IOError; + format!("Path is not a file: {}", path_str), + "Path points to a directory or special file, not a regular file" + ); + + // 检查文件大小 + let metadata = path.metadata().map_err(|e| { + error!(ErrorKind::IOError) + .attach_printable(format!("Failed to read file metadata: {}", e)) + })?; + let file_size = metadata.len(); + + ensure!(file_size > 0, ErrorKind::IOError; + format!("File is empty: {}", path_str), + "Model file has zero size" + ); + + ensure!(file_size >= 12, ErrorKind::IOError; + format!("File too small: {} bytes", file_size), + format!("Model file {} is too small ({} bytes, minimum 12 bytes required for MNN model)", path_str, file_size) + ); + + // 验证路径可以转换为 C 字符串 + let path_c_str = path.to_str().ok_or_else(|| { + error!(ErrorKind::AsciiError).attach_printable(format!( + "Path contains invalid UTF-8 characters: {}", + path_str + )) + })?; + + let c_path = std::ffi::CString::new(path_c_str) + .change_context(ErrorKind::AsciiError) + .attach_printable(format!("Path contains null bytes: {}", path_str))?; + let interpreter = unsafe { mnn_sys::Interpreter_createFromFile(c_path.as_ptr()) }; - ensure!(!interpreter.is_null(), ErrorKind::InterpreterError; "Failed to create interpreter", "Interpreter_createFromFile returned null"); + ensure!(!interpreter.is_null(), ErrorKind::InterpreterError; + "Failed to create interpreter", + format!("Interpreter_createFromFile returned null for file: {}. File size: {} bytes. Possible causes: invalid MNN model format, corrupted file, unsupported model version, or insufficient memory", path_str, file_size) + ); + Ok(Self { inner: interpreter, __marker: PhantomData, @@ -172,9 +216,17 @@ impl Interpreter { pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result { let bytes = bytes.as_ref(); let size = bytes.len(); + + // 添加输入验证 + ensure!(size > 0, ErrorKind::InterpreterError; "Empty buffer provided", format!("Buffer size is 0")); + ensure!(size >= 12, ErrorKind::InterpreterError; "Buffer too small", format!("Buffer size {} is too small for MNN model (minimum 12 bytes)", size)); + let interpreter = unsafe { mnn_sys::Interpreter_createFromBuffer(bytes.as_ptr().cast(), size) }; - ensure!(!interpreter.is_null(), ErrorKind::InterpreterError; "Failed to create interpreter", "Interpreter_createFromBuffer returned null"); + ensure!(!interpreter.is_null(), ErrorKind::InterpreterError; + "Failed to create interpreter", + format!("Interpreter_createFromBuffer returned null. Buffer size: {} bytes. Possible causes: invalid MNN model format, corrupted data, or unsupported model version", size) + ); Ok(Self { inner: interpreter, __marker: PhantomData, diff --git a/tests/assets/realesr.mnn b/tests/assets/realesr.mnn index 7edda62..b624ddf 100644 Binary files a/tests/assets/realesr.mnn and b/tests/assets/realesr.mnn differ diff --git a/tests/assets/resizing.mnn b/tests/assets/resizing.mnn index 1bfcd84..058a789 100644 Binary files a/tests/assets/resizing.mnn and b/tests/assets/resizing.mnn differ diff --git a/tests/async_test.rs b/tests/async_test.rs new file mode 100644 index 0000000..28570f9 --- /dev/null +++ b/tests/async_test.rs @@ -0,0 +1,190 @@ +// Tests to diagnose async interpreter creation issues + +// ============================================================================ +// Synchronous baseline tests +// ============================================================================ + +#[cfg(test)] +#[test] +fn test_from_file_sync_baseline() { + println!("\n=== Baseline test: from_file in synchronous context ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + match mnn::Interpreter::from_file(model_path) { + Ok(_interpreter) => { + println!("✓ Sync call succeeded"); + } + Err(e) => { + println!("✗ Sync call failed: {:?}", e); + panic!("Baseline test failed"); + } + } +} + +#[cfg(test)] +#[test] +fn test_from_bytes_sync_baseline() { + println!("\n=== Baseline test: from_bytes in synchronous context ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + let bytes = std::fs::read(model_path).expect("Failed to read model file"); + + println!("Model file size: {} bytes", bytes.len()); + + match mnn::Interpreter::from_bytes(&bytes) { + Ok(_interpreter) => { + println!("✓ Sync call succeeded"); + } + Err(e) => { + println!("✗ Sync call failed: {:?}", e); + panic!("Baseline test failed"); + } + } +} + +#[cfg(test)] +#[test] +fn test_from_file_in_thread() { + println!("\n=== Test: from_file in std::thread::spawn ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn").to_path_buf(); + + let handle = std::thread::spawn(move || { + println!("Thread ID: {:?}", std::thread::current().id()); + mnn::Interpreter::from_file(model_path) + }); + + match handle.join() { + Ok(Ok(_interpreter)) => { + println!("✓ Thread spawn succeeded"); + } + Ok(Err(e)) => { + println!("✗ Thread spawn failed: {:?}", e); + panic!("Thread test failed"); + } + Err(e) => { + println!("✗ Thread panic: {:?}", e); + panic!("Thread panicked"); + } + } +} + +#[cfg(test)] +#[test] +fn test_from_bytes_in_thread() { + println!("\n=== Test: from_bytes in std::thread::spawn ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + let bytes = std::fs::read(model_path).expect("Failed to read model file"); + + println!("Model file size: {} bytes", bytes.len()); + + let handle = std::thread::spawn(move || { + println!("Thread ID: {:?}", std::thread::current().id()); + mnn::Interpreter::from_bytes(bytes) + }); + + match handle.join() { + Ok(Ok(_interpreter)) => { + println!("✓ Thread spawn succeeded"); + } + Ok(Err(e)) => { + println!("✗ Thread spawn failed: {:?}", e); + panic!("Thread test failed"); + } + Err(e) => { + println!("✗ Thread panic: {:?}", e); + panic!("Thread panicked"); + } + } +} + +#[cfg(test)] +#[test] +fn test_multiple_threads_from_file() { + println!("\n=== Test: Multiple threads creating interpreters from file ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + let handles: Vec<_> = (0..3) + .map(|i| { + let path = model_path.to_path_buf(); + std::thread::spawn(move || { + println!("Thread {} starting", i); + let result = mnn::Interpreter::from_file(path); + println!("Thread {} result: {}", i, result.is_ok()); + result + }) + }) + .collect(); + + let results: Vec<_> = handles + .into_iter() + .enumerate() + .map(|(i, h)| match h.join() { + Ok(Ok(_)) => { + println!("✓ Thread {} succeeded", i); + true + } + Ok(Err(e)) => { + println!("✗ Thread {} failed: {:?}", i, e); + false + } + Err(e) => { + println!("✗ Thread {} panicked: {:?}", i, e); + false + } + }) + .collect(); + + let success_count = results.iter().filter(|&&x| x).count(); + println!("\n{}/{} threads succeeded", success_count, results.len()); + + if success_count < results.len() { + panic!("Some threads failed to create interpreter"); + } +} + +#[cfg(test)] +#[test] +fn test_multiple_threads_from_bytes() { + println!("\n=== Test: Multiple threads creating interpreters from bytes ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + let bytes = std::fs::read(model_path).expect("Failed to read model file"); + + println!("Model file size: {} bytes", bytes.len()); + + let handles: Vec<_> = (0..3) + .map(|i| { + let bytes_clone = bytes.clone(); + std::thread::spawn(move || { + println!("Thread {} starting", i); + let result = mnn::Interpreter::from_bytes(bytes_clone); + println!("Thread {} result: {}", i, result.is_ok()); + result + }) + }) + .collect(); + + let results: Vec<_> = handles + .into_iter() + .enumerate() + .map(|(i, h)| match h.join() { + Ok(Ok(_)) => { + println!("✓ Thread {} succeeded", i); + true + } + Ok(Err(e)) => { + println!("✗ Thread {} failed: {:?}", i, e); + false + } + Err(e) => { + println!("✗ Thread {} panicked: {:?}", i, e); + false + } + }) + .collect(); + + let success_count = results.iter().filter(|&&x| x).count(); + println!("\n{}/{} threads succeeded", success_count, results.len()); + + if success_count < results.len() { + panic!("Some threads failed to create interpreter"); + } +} diff --git a/tests/async_tokio_test.rs b/tests/async_tokio_test.rs new file mode 100644 index 0000000..96ef98e --- /dev/null +++ b/tests/async_tokio_test.rs @@ -0,0 +1,203 @@ +// Async tests with tokio to diagnose interpreter creation issues + +#[cfg(test)] +#[tokio::test] +async fn test_from_file_in_tokio_direct() { + println!("\n=== Test: from_file called directly in tokio async context ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + println!("Tokio thread: {:?}", std::thread::current().id()); + + match mnn::Interpreter::from_file(model_path) { + Ok(_interpreter) => { + println!("✓ Direct async call succeeded"); + } + Err(e) => { + println!("✗ Direct async call failed: {:?}", e); + panic!("Direct async call failed"); + } + } +} + +#[cfg(test)] +#[tokio::test] +async fn test_from_bytes_in_tokio_direct() { + println!("\n=== Test: from_bytes called directly in tokio async context ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + let bytes = std::fs::read(model_path).expect("Failed to read model file"); + + println!("Tokio thread: {:?}", std::thread::current().id()); + println!("Model file size: {} bytes", bytes.len()); + + match mnn::Interpreter::from_bytes(&bytes) { + Ok(_interpreter) => { + println!("✓ Direct async call succeeded"); + } + Err(e) => { + println!("✗ Direct async call failed: {:?}", e); + panic!("Direct async call failed"); + } + } +} + +#[cfg(test)] +#[tokio::test] +async fn test_from_file_with_spawn_blocking() { + println!("\n=== Test: from_file with tokio::task::spawn_blocking ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn").to_path_buf(); + + println!("Main tokio thread: {:?}", std::thread::current().id()); + + match tokio::task::spawn_blocking(move || { + println!("Blocking thread: {:?}", std::thread::current().id()); + mnn::Interpreter::from_file(model_path) + }) + .await + { + Ok(Ok(_interpreter)) => { + println!("✓ spawn_blocking succeeded"); + } + Ok(Err(e)) => { + println!("✗ spawn_blocking failed: {:?}", e); + panic!("spawn_blocking failed"); + } + Err(e) => { + println!("✗ Task join failed: {:?}", e); + panic!("Task join failed"); + } + } +} + +#[cfg(test)] +#[tokio::test] +async fn test_from_bytes_with_spawn_blocking() { + println!("\n=== Test: from_bytes with tokio::task::spawn_blocking ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + let bytes = std::fs::read(model_path).expect("Failed to read model file"); + + println!("Main tokio thread: {:?}", std::thread::current().id()); + println!("Model file size: {} bytes", bytes.len()); + + match tokio::task::spawn_blocking(move || { + println!("Blocking thread: {:?}", std::thread::current().id()); + mnn::Interpreter::from_bytes(bytes) + }) + .await + { + Ok(Ok(_interpreter)) => { + println!("✓ spawn_blocking succeeded"); + } + Ok(Err(e)) => { + println!("✗ spawn_blocking failed: {:?}", e); + panic!("spawn_blocking failed"); + } + Err(e) => { + println!("✗ Task join failed: {:?}", e); + panic!("Task join failed"); + } + } +} + +#[cfg(test)] +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_from_file_multi_thread_runtime() { + println!("\n=== Test: from_file in multi-threaded tokio runtime ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + println!("Main tokio thread: {:?}", std::thread::current().id()); + + match mnn::Interpreter::from_file(model_path) { + Ok(_interpreter) => { + println!("✓ Multi-thread runtime succeeded"); + } + Err(e) => { + println!("✗ Multi-thread runtime failed: {:?}", e); + panic!("Multi-thread runtime failed"); + } + } +} + +#[cfg(test)] +#[tokio::test(flavor = "current_thread")] +async fn test_from_file_current_thread_runtime() { + println!("\n=== Test: from_file in current-thread tokio runtime ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + println!("Main tokio thread: {:?}", std::thread::current().id()); + + match mnn::Interpreter::from_file(model_path) { + Ok(_interpreter) => { + println!("✓ Current-thread runtime succeeded"); + } + Err(e) => { + println!("✗ Current-thread runtime failed: {:?}", e); + panic!("Current-thread runtime failed"); + } + } +} + +#[cfg(test)] +#[tokio::test] +async fn test_multiple_concurrent_from_file() { + println!("\n=== Test: Multiple concurrent from_file calls ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + let mut handles = vec![]; + + for i in 0..3 { + let path = model_path.to_path_buf(); + let handle = tokio::task::spawn_blocking(move || { + println!("Task {} on thread: {:?}", i, std::thread::current().id()); + let result = mnn::Interpreter::from_file(path); + println!("Task {} result: {}", i, result.is_ok()); + result + }); + handles.push(handle); + } + + let mut success_count = 0; + for (i, handle) in handles.into_iter().enumerate() { + match handle.await { + Ok(Ok(_)) => { + println!("✓ Task {} succeeded", i); + success_count += 1; + } + Ok(Err(e)) => { + println!("✗ Task {} failed: {:?}", i, e); + } + Err(e) => { + println!("✗ Task {} join failed: {:?}", i, e); + } + } + } + + println!("\n{}/3 tasks succeeded", success_count); + + if success_count < 3 { + panic!("Some tasks failed"); + } +} + +#[cfg(test)] +#[tokio::test] +async fn test_from_file_after_await_point() { + println!("\n=== Test: from_file after an .await point ==="); + let model_path = std::path::Path::new("tests/assets/realesr.mnn"); + + println!("Before await - thread: {:?}", std::thread::current().id()); + + // 引入一个 await 点 + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + + println!("After await - thread: {:?}", std::thread::current().id()); + + match mnn::Interpreter::from_file(model_path) { + Ok(_interpreter) => { + println!("✓ After await point succeeded"); + } + Err(e) => { + println!("✗ After await point failed: {:?}", e); + panic!("After await point failed"); + } + } +}