diff --git a/CMakeLists.txt b/CMakeLists.txt index c4a2591..3360c08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ option(LIBICSNEO_ENABLE_TCP "Enable devices which communicate over TCP" OFF) option(LIBICSNEO_ENABLE_FTD3XX "Enable devices which communicate over USB FTD3XX" ON) option(LIBICSNEO_ENABLE_BINDINGS_PYTHON "Enable Python library" OFF) +option(LIBICSNEO_ENABLE_BINDINGS_RUST "Enable Rust library" OFF) if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index d243d3b..a6e9da7 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -1,3 +1,18 @@ if(LIBICSNEO_ENABLE_BINDINGS_PYTHON) add_subdirectory(python) +endif() + +if(LIBICSNEO_ENABLE_BINDINGS_RUST) + include(FetchContent) + + FetchContent_Declare( + Corrosion + GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git + GIT_TAG v0.5 # Optionally specify a commit hash, version tag or branch here + ) + FetchContent_MakeAvailable(Corrosion) + + corrosion_import_crate(MANIFEST_PATH ${CMAKE_SOURCE_DIR}/bindings/rust/icsneors/Cargo.toml) +else() + message(STATUS "Not building rust bindings") endif() \ No newline at end of file diff --git a/bindings/rust/icsneors/.gitignore b/bindings/rust/icsneors/.gitignore new file mode 100644 index 0000000..d01bd1a --- /dev/null +++ b/bindings/rust/icsneors/.gitignore @@ -0,0 +1,21 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/bindings/rust/icsneors/Cargo.toml b/bindings/rust/icsneors/Cargo.toml new file mode 100644 index 0000000..372579d --- /dev/null +++ b/bindings/rust/icsneors/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "icsneors" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[features] +static = [] + +[dependencies] + +[build-dependencies] +bindgen = "0.71.1" +path-clean = "1.0.1" +cmake = "0.1.52" +which = "7.0.0" \ No newline at end of file diff --git a/bindings/rust/icsneors/build.rs b/bindings/rust/icsneors/build.rs new file mode 100644 index 0000000..6bd9a0b --- /dev/null +++ b/bindings/rust/icsneors/build.rs @@ -0,0 +1,202 @@ +use cmake::Config; +use path_clean::{clean, PathClean}; +use std::{env, path::PathBuf}; + +fn libicsneo_path() -> PathBuf { + // Get the path of libicsneo + let path = std::env::var("LIBICSNEO_PATH") + .unwrap_or(format!("{}/../../../", env!("CARGO_MANIFEST_DIR"))); + let libicsneo_path = std::path::PathBuf::from(clean(&path)); + + libicsneo_path +} + +fn libicsneo_include_path() -> PathBuf { + let path = libicsneo_path().join("include"); + path.clean() +} + +fn libicsneo_header_path() -> PathBuf { + let path = libicsneo_include_path().join("icsneo").join("icsneo.h"); + path.clean() +} + +// Detects the cargo build profile, true = release, otherwise false +fn is_release_build() -> bool { + let profile = std::env::var("PROFILE").unwrap(); + match profile.as_str() { + "debug" => return false, + "release" => return true, + _ => return false, + } +} + +// returns the cmake build string that is normally passed to -DCMAKE_BUILD_TYPE= +fn cmake_build_config_type() -> String { + let build_config_type = if is_release_build() { + "Release" + } else { + if cfg!(target_os = "windows") { + // Rust runtime is linked with /MD on windows MSVC... MSVC takes Debug and forces /MDd + // https://www.reddit.com/r/rust/comments/dvmzo2/cargo_external_c_library_windows_debugrelease_hell/ + "RelWithDebInfo" + } else { + "Debug" + } + }; + build_config_type.to_string() +} + +// Build libicsneo through cmake, returns the build directory +fn build_libicsneo() -> PathBuf { + let libicsneo_path = libicsneo_path(); + // Check to make sure CMakeLists.txt exists + if !libicsneo_path.join("CMakeLists.txt").exists() { + panic!("CMakeLists.txt not found at {}", libicsneo_path.display()); + } + let build_config_type = cmake_build_config_type(); + // Run cmake on libicsneo + let mut config = Config::new(libicsneo_path.clone()); + let config = config + .build_target("ALL_BUILD") + // .define("LIBICSNEO_BUILD_ICSNEOC_STATIC:BOOL", "ON") + // .define("LIBICSNEO_BUILD_EXAMPLES:BOOL", "OFF") + // .define("LIBICSNEO_BUILD_ICSNEOLEGACY:BOOL", "OFF") + .profile(&build_config_type); + // Lets use ninja if it exists + let config = match which::which("ninja") { + Ok(_) => config.generator("Ninja Multi-Config").build_target("all"), + Err(_e) => config, + }; + config.build() +} + +fn setup_linker_libs(build_path: &PathBuf) { + let build_config_type = cmake_build_config_type(); + // output for lib path + println!( + "cargo:warning=build search path: {:?}", + build_path + .join(format!("build/{build_config_type}")) + .display() + ); + // icsneo lib/dll linker search path + println!( + "cargo:rustc-link-search=native={}", + build_path + .join(format!("build/{build_config_type}")) + .display() + ); + // fatfs linker search path and addition + println!( + "cargo:rustc-link-search=native={}/build/third-party/fatfs/{build_config_type}", + build_path.display() + ); + // libicsneo libraries + println!("cargo:rustc-link-lib=fatfs"); + println!("cargo:rustc-link-lib=static=icsneocpp"); + if cfg!(feature = "static") { + println!("cargo:rustc-link-lib=static=icsneo-static"); + } else { + println!("cargo:rustc-link-lib=dylib=icsneo"); + } + // Platform specific libraries + match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { + "windows" => { + // FTD3xx linker search path and addition + println!( + "cargo:rustc-link-search=native={}/build/_deps/ftdi3xx-src", + build_path.display() + ); + println!("cargo:rustc-link-lib=FTD3XX"); + } + "linux" => {} + "macos" => { + println!("cargo:rustc-link-lib=static=icsneo-static"); + println!("cargo:rustc-link-lib=framework=IOKit"); + println!("cargo:rustc-link-lib=framework=CoreFoundation"); + } + target_os => panic!("Target OS not supported: {target_os}"), + } +} + +fn prepare_git_submodule() { + // We don't need to checkout the submodule if we are using a custom libicsneo path + if std::env::var("LIBICSNEO_PATH").is_ok() { + println!("cargo:warning=Using custom LIBICSNEO_PATH, skipping checking out submodules"); + return; + } + let libicsneo_path = libicsneo_path(); + // This seems to not be needed when including this as a dependency? Why? + // checkout the submodule if needed + let output = std::process::Command::new("git") + .args(["submodule", "update", "--init"]) + .current_dir(libicsneo_path) + .output() + .expect("Failed to fetch git submodules!"); + // Make sure git was successful! + if !output.status.success() { + println!("cargo:warning=git return code: {}", output.status); + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + for line in stdout.split("\n") { + println!("cargo:warning=git stdout: {}", line); + } + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + for line in stderr.split("\n") { + println!("cargo:warning=git stderr: {}", line); + } + } +} + +fn generate_bindings() { + let header = libicsneo_header_path(); + let bindings = bindgen::Builder::default() + .header(header.to_str().unwrap()) + .default_enum_style(bindgen::EnumVariation::Rust { + non_exhaustive: false, + }) + .clang_args(&[format!("-I{}", libicsneo_include_path().display()).as_str()]) + .blocklist_file("stdint.h") + .blocklist_file("stdbool.h") + .use_core() + // .allowlist_function("icsneo_.*") + // .allowlist_type("neodevice_t") + // .allowlist_type("neonetid_t") + // .allowlist_type("neomessage_.*") + // .allowlist_type("neoversion_t") + // .allowlist_type("neoevent_t") + .formatter(bindgen::Formatter::Rustfmt) + .derive_default(true) + .derive_debug(true) + .derive_partialeq(true) + .derive_copy(true) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + //.clang_args(clang_args()) + .generate() + .expect("Unable to generate bindings"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + println!("cargo:warning=out_path: {:?}", out_path.display()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings"); + + let out_path = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} + +fn main() { + let header = libicsneo_header_path(); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed={}", header.to_str().unwrap()); + println!("cargo:rerun-if-env-changed=LIBMSVC_PATH"); + + prepare_git_submodule(); + generate_bindings(); + // We can skip building if its for docs.rs + if std::env::var("DOCS_RS").is_err() { + let build_directory = build_libicsneo(); + setup_linker_libs(&build_directory); + } +} diff --git a/bindings/rust/icsneors/src/lib.rs b/bindings/rust/icsneors/src/lib.rs new file mode 100644 index 0000000..275f647 --- /dev/null +++ b/bindings/rust/icsneors/src/lib.rs @@ -0,0 +1,20 @@ +// Suppress the flurry of warnings caused by using "C" naming conventions +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}