From fcd255405174731466de7916ba5e6b446bda591a Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 23 May 2026 20:25:32 -0400 Subject: [PATCH] Add rust bindings (#98) * add rust bindings to norm * modify PR description * remove PR description since it is redundant * remove weird character * remove __pycache__ and add __pycache__ to gitignore --------- Co-authored-by: cbowers --- .gitignore | 1 + src/rust/API_GUIDE.md | 248 ++++++++++ src/rust/Cargo.lock | 704 ++++++++++++++++++++++++++++ src/rust/Cargo.toml | 16 + src/rust/README.md | 210 +++++++++ src/rust/examples/data_recv.rs | 82 ++++ src/rust/examples/data_send.rs | 71 +++ src/rust/examples/file_recv.rs | 94 ++++ src/rust/examples/file_send.rs | 81 ++++ src/rust/examples/stream_example.rs | 207 ++++++++ src/rust/examples/stream_recv.rs | 102 ++++ src/rust/examples/stream_send.rs | 92 ++++ src/rust/norm-sys/Cargo.toml | 18 + src/rust/norm-sys/build.rs | 84 ++++ src/rust/norm-sys/src/lib.rs | 24 + src/rust/norm-sys/wrapper.h | 4 + src/rust/norm/Cargo.toml | 37 ++ src/rust/norm/build.rs | 7 + src/rust/norm/src/error.rs | 187 ++++++++ src/rust/norm/src/event.rs | 53 +++ src/rust/norm/src/instance.rs | 217 +++++++++ src/rust/norm/src/lib.rs | 51 ++ src/rust/norm/src/multicast.rs | 339 ++++++++++++++ src/rust/norm/src/node.rs | 77 +++ src/rust/norm/src/object.rs | 316 +++++++++++++ src/rust/norm/src/session.rs | 459 ++++++++++++++++++ src/rust/norm/src/types.rs | 525 +++++++++++++++++++++ src/rust/target/.rustc_info.json | 1 + src/rust/target/CACHEDIR.TAG | 3 + src/rust/waf_rust.py | 131 ++++++ wscript | 32 +- 31 files changed, 4472 insertions(+), 1 deletion(-) create mode 100644 src/rust/API_GUIDE.md create mode 100644 src/rust/Cargo.lock create mode 100644 src/rust/Cargo.toml create mode 100644 src/rust/README.md create mode 100644 src/rust/examples/data_recv.rs create mode 100644 src/rust/examples/data_send.rs create mode 100644 src/rust/examples/file_recv.rs create mode 100644 src/rust/examples/file_send.rs create mode 100644 src/rust/examples/stream_example.rs create mode 100644 src/rust/examples/stream_recv.rs create mode 100644 src/rust/examples/stream_send.rs create mode 100644 src/rust/norm-sys/Cargo.toml create mode 100644 src/rust/norm-sys/build.rs create mode 100644 src/rust/norm-sys/src/lib.rs create mode 100644 src/rust/norm-sys/wrapper.h create mode 100644 src/rust/norm/Cargo.toml create mode 100644 src/rust/norm/build.rs create mode 100644 src/rust/norm/src/error.rs create mode 100644 src/rust/norm/src/event.rs create mode 100644 src/rust/norm/src/instance.rs create mode 100644 src/rust/norm/src/lib.rs create mode 100644 src/rust/norm/src/multicast.rs create mode 100644 src/rust/norm/src/node.rs create mode 100644 src/rust/norm/src/object.rs create mode 100644 src/rust/norm/src/session.rs create mode 100644 src/rust/norm/src/types.rs create mode 100644 src/rust/target/.rustc_info.json create mode 100644 src/rust/target/CACHEDIR.TAG create mode 100644 src/rust/waf_rust.py diff --git a/.gitignore b/.gitignore index b6c877f..041949d 100755 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ norp/makefiles/norp # python build files src/pynorm.egg-info +**/__pycache__/ # build java files *.class diff --git a/src/rust/API_GUIDE.md b/src/rust/API_GUIDE.md new file mode 100644 index 0000000..b2bdfc5 --- /dev/null +++ b/src/rust/API_GUIDE.md @@ -0,0 +1,248 @@ +# NORM Rust Bindings API Guide + +This guide provides a detailed overview of the main components of the NORM Rust bindings API. + +## Core Components + +The NORM Rust API follows a hierarchical structure similar to the underlying C API but with +Rust idioms and safety guarantees: + +``` +Instance + └── Session + ├── Object (Data) + ├── Object (File) + └── Object (Stream) +``` + +### Instance + +`Instance` is the top-level object that represents a NORM protocol instance. It's the starting point +for all NORM operations. + +```rust +use norm::Instance; + +// Create a new NORM instance +let instance = Instance::new(false)?; // false = no priority boost + +// Set cache directory for receiving files +instance.set_cache_directory("/tmp/norm")?; + +// Process events +for event in instance.events() { + // Handle events +} +``` + +### Session + +A `Session` represents a NORM protocol session, which can operate in sender mode, receiver mode, or both. + +```rust +use norm::{Instance, Session}; + +let instance = Instance::new(false)?; + +// Create a session with address, port, and node ID +let session = instance.create_session("224.1.2.3", 6003, 1)?; + +// Configure session +session + .set_tx_rate(1_000_000.0) // 1 Mbps + .set_ttl(64)? + .set_loopback(true)?; + +// Start sender +session.start_sender( + rand::random(), // Session ID + 1024 * 1024, // Buffer space (1 MB) + 1400, // Segment size + 64, // Data segments per block + 16, // Parity segments per block + None, // FEC ID (default = 0) +)?; + +// Or start receiver +session.start_receiver(1024 * 1024)?; +``` + +### Objects + +NORM supports three types of objects for data transfer: + +1. **Data Objects**: For memory buffer transfers +2. **File Objects**: For file transfers +3. **Stream Objects**: For continuous data streaming + +```rust +// Send data +let data = b"Hello, NORM!"; +let data_obj = session.data_enqueue(data, Some(b"Info"))?; + +// Send file +let file_obj = session.file_enqueue("/path/to/file.txt", Some(b"file.txt"))?; + +// Open stream +let stream_obj = session.stream_open(64 * 1024, Some(b"Stream info"))?; +``` + +### Events + +NORM uses an event-based model for signaling state changes. The Rust bindings provide an iterator-based approach for event handling. + +```rust +// Process events using iterator +for event in instance.events() { + match event.event_type { + EventType::RxObjectCompleted => { + let object = Object::from_handle_unowned(event.object); + + // Handle based on object type + match object.get_type() { + ObjectType::Data => { + if let Ok(data) = object.access_data() { + println!("Received data: {:?}", data); + } + }, + ObjectType::File => { + if let Ok(info) = object.get_info() { + println!("Received file: {}", String::from_utf8_lossy(&info)); + } + }, + _ => {} + } + }, + EventType::TxObjectSent => println!("Object sent"), + _ => {} // Handle other events + } +} +``` + +## Multicast Configuration + +The Rust bindings provide an ergonomic API for configuring multicast: + +```rust +use norm::{multicast, MulticastExt}; + +// Configure multicast with builder pattern +let config = multicast!("224.1.2.3", 6003, { + ttl: 64, + interface: "eth0", + loopback: true, + ssm_source: "192.168.1.1", +}); + +// Apply configuration to session +session.with_multicast(&config)?; +``` + +## Error Handling + +All operations that might fail return a `Result` type: + +```rust +match instance.set_cache_directory("/nonexistent/path") { + Ok(()) => println!("Cache directory set"), + Err(e) => match e { + Error::FileError(msg) => println!("File error: {}", msg), + Error::InvalidParameter => println!("Invalid parameter"), + _ => println!("Other error: {:?}", e), + } +} +``` + +## Ownership and Lifetimes + +The Rust bindings use RAII (Resource Acquisition Is Initialization) to ensure proper resource management: + +- `Instance`, `Session`, and `Object` implement `Drop` to automatically clean up resources +- Objects created directly have ownership and will be freed when dropped +- Objects obtained from events are not owned and are marked as such + +```rust +// Owned object from direct creation +let data_obj = session.data_enqueue(data, None)?; +// Will be released when data_obj goes out of scope + +// Non-owned object from event +let object = Object::from_handle_unowned(event.object); +// Will NOT be released when object goes out of scope +``` + +## Utility Functions + +The API provides several utility functions: + +```rust +// Check if an address is a multicast address +let is_mcast = is_multicast_address("224.1.2.3"); + +// Get NORM version +let (major, minor, patch) = norm::version(); +``` + +## Advanced Features + +### Custom Memory Allocation + +```rust +unsafe { + instance.set_allocation_functions( + my_alloc_function, + my_free_function + ); +} +``` + +### File Transfers + +When receiving files, you must set a cache directory: + +```rust +instance.set_cache_directory("/tmp/norm_files")?; +``` + +### Stream Management + +For stream objects, additional methods are available on the `Object` type: + +```rust +// Open a stream +let stream = session.stream_open(64 * 1024, Some(b"Stream info"))?; + +// Write to stream +let bytes_written = stream.stream_write(b"Hello, stream!")?; + +// Check if stream has space for more data +if stream.stream_has_vacancy()? { + stream.stream_write(b"More data")?; +} + +// Mark end of message +stream.stream_mark_eom()?; + +// Flush stream with end-of-message marker +stream.stream_flush(true, FlushMode::Passive)?; + +// Read from stream (receiver side) +let mut buffer = vec![0u8; 1024]; +let bytes_read = stream.stream_read(&mut buffer)?; + +// Seek to next message start +if stream.stream_seek_msg_start()? { + println!("Found next message"); +} + +// Close stream gracefully +stream.stream_close(true)?; +``` + +## Best Practices + +1. **Always check return values** - Use the `?` operator or explicitly handle errors +2. **Process all events** - Use the event iterator to process all events +3. **Close resources properly** - Let RAII handle cleanup or explicitly call `stop_sender()`/`stop_receiver()` +4. **Configure multicast correctly** - Use the ergonomic multicast API +5. **Use appropriate buffer sizes** - Match buffer sizes to your application's needs \ No newline at end of file diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock new file mode 100644 index 0000000..700e554 --- /dev/null +++ b/src/rust/Cargo.lock @@ -0,0 +1,704 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "norm" +version = "1.5.10" +dependencies = [ + "libc", + "norm-sys", + "rand", + "thiserror", + "tokio", +] + +[[package]] +name = "norm-sys" +version = "1.5.10" +dependencies = [ + "bindgen", + "cc", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..541317c --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +resolver = "2" +members = [ + "norm-sys", + "norm", +] + +[workspace.package] +version = "1.5.10" # Matching NORM's version +edition = "2021" +license = "BSD-3-Clause" +repository = "https://github.com/USNavalResearchLaboratory/norm" + +[workspace.dependencies] +norm-sys = { path = "norm-sys", version = "1.5.10" } +rand = "0.8" \ No newline at end of file diff --git a/src/rust/README.md b/src/rust/README.md new file mode 100644 index 0000000..3464004 --- /dev/null +++ b/src/rust/README.md @@ -0,0 +1,210 @@ +# NORM Rust Bindings + +Safe, idiomatic Rust bindings for the [NORM](https://github.com/USNavalResearchLaboratory/norm) (NACK-Oriented Reliable Multicast) protocol library. + +## Overview + +These Rust bindings provide a safe and ergonomic interface to the NORM C library. NORM is a reliable multicast protocol developed by the US Naval Research Laboratory that provides end-to-end reliable transport of data over IP multicast or unicast. + +## Features + +- **RAII Resource Management**: Safe wrappers ensure proper cleanup of NORM resources +- **Ergonomic API**: Builder patterns for multicast configuration +- **Iterator-based Event Handling**: Simple event loops using Rust iterators +- **Type Safety**: Rust enums for NORM constants and options +- **Error Handling**: Rich error types with descriptive messages + +## Installation + +### Prerequisites + +- Rust 1.70 or later +- NORM library (installed or available for linking) + +### Building from Source + +```bash +# Clone the NORM repository +git clone https://github.com/USNavalResearchLaboratory/norm.git +cd norm + +# Build NORM with Rust bindings +./waf configure --build-rust +./waf build + +# Or for release mode +./waf configure --build-rust --rust-release +./waf build +``` + +### Using as a Dependency + +```toml +# Cargo.toml +[dependencies] +norm = "1.5.10" # Replace with actual version +``` + +## Usage Examples + +### Data Transfer + +```rust +use norm::{Instance, multicast, MulticastExt, EventType, Result}; + +fn main() -> Result<()> { + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Create a session + let session = instance.create_session("224.1.2.3", 6003, 1)?; + + // Configure multicast with the ergonomic API + let config = multicast!("224.1.2.3", 6003, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&config)?; + + // Start sender + session + .set_tx_rate(1_000_000.0) + .start_sender(rand::random(), 1024*1024, 1400, 64, 16, None)?; + + // Send data + let data = b"Hello, NORM!"; + session.data_enqueue(data, None)?; + + // Process events + for event in instance.events() { + if event.event_type == EventType::TxQueueEmpty { + break; + } + } + + Ok(()) +} +``` + +### Stream Transfer + +```rust +use norm::{Instance, multicast, MulticastExt, FlushMode, Result}; + +fn main() -> Result<()> { + let instance = Instance::new(false)?; + let session = instance.create_session("224.1.2.3", 6003, 1)?; + + let config = multicast!("224.1.2.3", 6003, { ttl: 64, loopback: true }); + session.with_multicast(&config)?; + + session.set_tx_rate(1_000_000.0) + .start_sender(rand::random(), 1024*1024, 1400, 64, 16, None)?; + + // Open a stream + let stream = session.stream_open(64 * 1024, Some(b"My stream"))?; + + // Write multiple messages + stream.stream_write(b"Message 1")?; + stream.stream_mark_eom()?; + + stream.stream_write(b"Message 2")?; + stream.stream_mark_eom()?; + + // Flush and close + stream.stream_flush(true, FlushMode::Active)?; + stream.stream_close(true)?; + + Ok(()) +} +``` + +### Multicast Configuration + +```rust +// Method 1: Builder pattern +let config = MulticastConfig::new("224.1.2.3", 6003) + .ttl(64) + .interface("eth0") + .loopback(true); + +// Method 2: Convenient macro +let config = multicast!("224.1.2.3", 6003, { + ttl: 64, + interface: "eth0", + loopback: true, +}); + +// Apply to session +session.with_multicast(&config)?; +``` + +### Event Handling + +```rust +// Iterator-based event handling +for event in instance.events() { + match event.event_type { + EventType::RxObjectCompleted => { + let object = Object::from_handle_unowned(event.object); + if let Ok(data) = object.access_data() { + println!("Received: {:?}", data); + } + }, + EventType::TxObjectSent => println!("Object sent"), + _ => {} // Ignore other events + } +} +``` + +## Architecture + +The bindings are split into two crates: + +- **norm-sys**: Low-level FFI bindings generated with bindgen +- **norm**: Safe, idiomatic Rust wrappers with RAII and error handling + +## API Documentation + +For full API documentation, build the docs with: + +```bash +cd src/rust +cargo doc --open +``` + +Or when using waf: + +```bash +./waf configure --build-rust --rust-docs +./waf build +``` + +## Running Tests + +Due to the way the NORM library is built, you may need to set the library path when running tests: + +```bash +# On macOS +export DYLD_LIBRARY_PATH=/path/to/norm/build:/path/to/norm/build/protolib:$DYLD_LIBRARY_PATH +cargo test + +# On Linux +export LD_LIBRARY_PATH=/path/to/norm/build:/path/to/norm/build/protolib:$LD_LIBRARY_PATH +cargo test + +# Or run directly +DYLD_LIBRARY_PATH=/path/to/norm/build:/path/to/norm/build/protolib cargo test +``` + +Alternatively, use the waf build system which handles this automatically: + +```bash +./waf configure --build-rust +./waf build +./waf test # If test target is configured +``` + +## License + +These bindings are distributed under the same BSD-3-Clause license as the NORM library itself. \ No newline at end of file diff --git a/src/rust/examples/data_recv.rs b/src/rust/examples/data_recv.rs new file mode 100644 index 0000000..1a1e024 --- /dev/null +++ b/src/rust/examples/data_recv.rs @@ -0,0 +1,82 @@ +use norm::{Instance, multicast, MulticastExt, EventType, ObjectType, Result}; +use std::env; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + let (address, port) = if args.len() > 2 { + (args[1].as_str(), args[2].parse::().unwrap_or(6003)) + } else { + println!("Using default multicast address 224.1.2.3:6003"); + println!("Usage: {}
", args[0]); + ("224.1.2.3", 6003) + }; + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Set the cache directory for file reception (required for file objects) + instance.set_cache_directory("/tmp/norm")?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 2)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Start the receiver with 1MB buffer + session.start_receiver(1024 * 1024)?; + + println!("NORM receiver started on {}:{}", address, port); + println!("Waiting for data..."); + + // Event loop - wait for data to be received + for event in instance.events() { + match event.event_type { + EventType::RemoteSenderNew => { + println!("New sender connected"); + } + EventType::RxObjectNew => { + println!("New object received, waiting for completion..."); + } + EventType::RxObjectCompleted => { + // Check if it's a data object + let obj_handle = event.object; + let object = norm::Object::from_handle_unowned(obj_handle); + + if object.get_type() == ObjectType::Data { + // Access the data + if let Ok(data) = object.access_data() { + println!("Received data: {:?}", String::from_utf8_lossy(data)); + + // Check if there's info data + if object.has_info() { + if let Ok(info) = object.get_info() { + println!("Info data: {:?}", String::from_utf8_lossy(&info)); + } + } + + // Exit after receiving one data object + break; + } else { + println!("Failed to access data"); + } + } + } + _ => { + // Ignore other events + } + } + } + + // Clean up (this happens automatically via Drop, but being explicit) + session.stop_receiver(); + + println!("Data receive example completed"); + + Ok(()) +} \ No newline at end of file diff --git a/src/rust/examples/data_send.rs b/src/rust/examples/data_send.rs new file mode 100644 index 0000000..89408ce --- /dev/null +++ b/src/rust/examples/data_send.rs @@ -0,0 +1,71 @@ +use norm::{Instance, multicast, MulticastExt, EventType, Result}; +use std::time::Duration; +use std::{thread, env}; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + let (address, port) = if args.len() > 2 { + (args[1].as_str(), args[2].parse::().unwrap_or(6003)) + } else { + println!("Using default multicast address 224.1.2.3:6003"); + println!("Usage: {}
", args[0]); + ("224.1.2.3", 6003) + }; + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 1)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Set the transmission rate (in bits per second) + session.set_tx_rate(1_000_000.0); + + // Start the sender with 1MB buffer, 1400 byte segments, 64 data segments per block, 16 parity segments + let session_id = rand::random::(); + session.start_sender(session_id, 1024 * 1024, 1400, 64, 16, None)?; + + // Prepare some data to send + let data = b"Hello, NORM! This is a test message sent using the Rust bindings."; + let info = b"Example data message"; + + println!("Sending data: {:?}", String::from_utf8_lossy(data)); + + // Enqueue the data for transmission + let _data_obj = session.data_enqueue(data, Some(info))?; + + println!("Data enqueued, waiting for transmission to complete..."); + + // Event loop - wait for transmission to complete + for event in instance.events() { + match event.event_type { + EventType::TxObjectSent => { + println!("Object sent successfully"); + } + EventType::TxQueueEmpty => { + println!("Transmission queue empty"); + // Short delay before exiting to allow any final processing + thread::sleep(Duration::from_millis(500)); + break; + } + _ => { + // Ignore other events + } + } + } + + // Clean up (this happens automatically via Drop, but being explicit) + session.stop_sender(); + + println!("Data send example completed"); + + Ok(()) +} \ No newline at end of file diff --git a/src/rust/examples/file_recv.rs b/src/rust/examples/file_recv.rs new file mode 100644 index 0000000..2678815 --- /dev/null +++ b/src/rust/examples/file_recv.rs @@ -0,0 +1,94 @@ +use norm::{Instance, Session, multicast, MulticastExt, EventType, ObjectType, Result}; +use std::time::Duration; +use std::{thread, env, path::Path}; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + let (address, port) = if args.len() > 2 { + (args[1].as_str(), args[2].parse::().unwrap_or(6003)) + } else { + println!("Using default multicast address 224.1.2.3:6003"); + println!("Usage: {}
", args[0]); + ("224.1.2.3", 6003) + }; + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Set the cache directory for file reception + // IMPORTANT: This is required for receiving file objects + let cache_dir = "/tmp/norm"; + println!("Setting cache directory to: {}", cache_dir); + instance.set_cache_directory(cache_dir)?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 2)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Start the receiver with 4MB buffer + session.start_receiver(4 * 1024 * 1024)?; + + println!("NORM receiver started on {}:{}", address, port); + println!("Waiting for file..."); + + // Event loop - wait for file to be received + for event in instance.events() { + match event.event_type { + EventType::RemoteSenderNew => { + println!("New sender connected"); + } + EventType::RxObjectNew => { + let object = norm::Object::from_handle_unowned(event.object); + if object.get_type() == ObjectType::File { + println!("New file being received..."); + + // Try to get info data which should contain the file name + if let Ok(info) = object.get_info() { + if !info.is_empty() { + println!("File name: {}", String::from_utf8_lossy(&info)); + } + } + } + } + EventType::RxObjectCompleted => { + let object = norm::Object::from_handle_unowned(event.object); + if object.get_type() == ObjectType::File { + println!("File received successfully!"); + + // The file will be in the cache directory + // You can find out its name from the info data + if let Ok(info) = object.get_info() { + if !info.is_empty() { + let filename = String::from_utf8_lossy(&info); + let filepath = format!("{}/{}", cache_dir, filename); + println!("File saved to: {}", filepath); + + // Report file size + println!("File size: {} bytes", object.size()); + + // Exit after receiving one file + break; + } + } + } + } + _ => { + // Ignore other events + } + } + } + + // Clean up + session.stop_receiver(); + + println!("File receive example completed"); + + Ok(()) +} \ No newline at end of file diff --git a/src/rust/examples/file_send.rs b/src/rust/examples/file_send.rs new file mode 100644 index 0000000..ec66312 --- /dev/null +++ b/src/rust/examples/file_send.rs @@ -0,0 +1,81 @@ +use norm::{Instance, Session, multicast, MulticastExt, EventType, Result}; +use std::time::Duration; +use std::{thread, env, path::Path}; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + if args.len() < 2 { + println!("Usage: {} [address] [port]", args[0]); + return Err(norm::Error::InvalidParameter); + } + + let file_path = &args[1]; + let address = if args.len() > 2 { &args[2] } else { "224.1.2.3" }; + let port = if args.len() > 3 { args[3].parse::().unwrap_or(6003) } else { 6003 }; + + // Verify the file exists + if !Path::new(file_path).exists() { + println!("File not found: {}", file_path); + return Err(norm::Error::FileError("File not found".to_string())); + } + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 1)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Set the transmission rate (in bits per second) + session.set_tx_rate(25_000_000.0); // 25 Mbps + + // Start the sender + let session_id = rand::random::(); + session.start_sender(session_id, 4 * 1024 * 1024, 1400, 64, 16, None)?; + + // Get just the file name to use as info data + let file_name = Path::new(file_path) + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + println!("Sending file: {} ({})", file_path, file_name); + + // Enqueue the file for transmission with the file name as info + let file_obj = session.file_enqueue(file_path, Some(file_name.as_bytes()))?; + + println!("File enqueued, waiting for transmission to complete..."); + + // Event loop - wait for transmission to complete + for event in instance.events() { + match event.event_type { + EventType::TxObjectSent => { + println!("File sent successfully"); + } + EventType::TxFlushCompleted => { + println!("Transmission flush completed"); + break; + } + EventType::RemoteSenderNew => { + println!("Receiver joined"); + } + _ => { + // Ignore other events + } + } + } + + // Clean up + session.stop_sender(); + + println!("File send example completed"); + + Ok(()) +} \ No newline at end of file diff --git a/src/rust/examples/stream_example.rs b/src/rust/examples/stream_example.rs new file mode 100644 index 0000000..c8374a6 --- /dev/null +++ b/src/rust/examples/stream_example.rs @@ -0,0 +1,207 @@ +use norm::{Instance, Session, multicast, MulticastExt, EventType, ObjectType, FlushMode, Result}; +use std::time::Duration; +use std::{thread, env}; +use std::str; + +// This example demonstrates a simple stream sender and receiver in one program +// It creates both sender and receiver threads for demonstration purposes +fn main() -> Result<()> { + let address = "224.1.2.3"; + let port = 6003; + + println!("NORM Stream Example"); + println!("-------------------"); + println!("Using multicast address: {}:{}", address, port); + + // Spawn receiver thread + let receiver_thread = thread::spawn(move || { + if let Err(e) = run_receiver(address, port) { + eprintln!("Receiver error: {:?}", e); + } + }); + + // Give the receiver a moment to start + thread::sleep(Duration::from_millis(500)); + + // Run sender in the main thread + if let Err(e) = run_sender(address, port) { + eprintln!("Sender error: {:?}", e); + } + + // Wait for receiver to finish + receiver_thread.join().expect("Receiver thread panicked"); + + println!("Stream example completed"); + Ok(()) +} + +fn run_sender(address: &str, port: u16) -> Result<()> { + // Create NORM instance and session for sender + let instance = Instance::new(false)?; + let session = instance.create_session(address, port, 1)?; + + // Configure multicast + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Set transmission rate + session.set_tx_rate(1_000_000.0); + + // Start sender + let session_id = rand::random::(); + session.start_sender(session_id, 1024 * 1024, 1400, 64, 16, None)?; + + // Open a stream with 64KB buffer + let stream_buffer_size = 64 * 1024; + let info = b"Example stream"; + let stream = session.stream_open(stream_buffer_size, Some(info))?; + + println!("Stream opened, sending messages..."); + + // Send 10 messages through the stream + for i in 1..=10 { + let message = format!("Stream message #{}", i); + println!("Sending: {}", message); + + // Write message to stream + let bytes_written = unsafe { + // Using the raw API here for simplicity + // In a real implementation, you'd want to create a safer wrapper + norm_sys::NormStreamWrite( + stream.handle(), + message.as_ptr() as *const i8, + message.len() as u32, + ) + }; + + if bytes_written < message.len() as u32 { + println!("Warning: Only wrote {} of {} bytes", bytes_written, message.len()); + } + + // Mark end of message and flush passively + unsafe { + norm_sys::NormStreamMarkEom(stream.handle()); + norm_sys::NormStreamFlush(stream.handle(), false, norm_sys::NormFlushMode_NORM_FLUSH_PASSIVE); + } + + // Small delay between messages + thread::sleep(Duration::from_millis(500)); + } + + // Close the stream gracefully + unsafe { + norm_sys::NormStreamClose(stream.handle(), true); + } + + println!("Stream closed, waiting for transmission to complete..."); + + // Wait for a short time to ensure all data is flushed + thread::sleep(Duration::from_secs(2)); + + // Clean up + session.stop_sender(); + + Ok(()) +} + +fn run_receiver(address: &str, port: u16) -> Result<()> { + // Create NORM instance and session for receiver + let instance = Instance::new(false)?; + let session = instance.create_session(address, port, 2)?; + + // Configure multicast + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Start receiver + session.start_receiver(1024 * 1024)?; + + println!("Receiver started, waiting for stream data..."); + + // Variables to track the current stream object + let mut current_stream = None; + let mut message_count = 0; + let mut timeout_count = 0; + + // Event loop + for event in instance.events() { + match event.event_type { + EventType::RemoteSenderNew => { + println!("New stream sender detected"); + } + EventType::RxObjectNew => { + let object = norm::Object::from_handle_unowned(event.object); + if object.get_type() == ObjectType::Stream { + println!("New stream object received"); + + // Store the stream object + current_stream = Some(event.object); + + // Check for info + if object.has_info() { + if let Ok(info) = object.get_info() { + println!("Stream info: {}", String::from_utf8_lossy(&info)); + } + } + } + } + EventType::RxObjectUpdated => { + // Stream data is available to read + if let Some(stream) = current_stream { + if event.object == stream { + // Read from the stream + let mut buffer = vec![0u8; 1024]; + let mut bytes_read = 0u32; + + let success = unsafe { + norm_sys::NormStreamRead( + stream, + buffer.as_mut_ptr() as *mut i8, + &mut bytes_read as *mut u32, + ) + }; + + if success && bytes_read > 0 { + buffer.truncate(bytes_read as usize); + println!("Received: {}", String::from_utf8_lossy(&buffer)); + message_count += 1; + + // Reset timeout counter when we get data + timeout_count = 0; + } + } + } + } + EventType::RxObjectCompleted => { + if let Some(stream) = current_stream { + if event.object == stream { + println!("Stream completed, received {} messages", message_count); + + // Exit when stream is completed + return Ok(()); + } + } + } + _ => { + // Check for timeout + timeout_count += 1; + if timeout_count > 10000 { + // If we haven't seen any activity for a while, exit + if message_count > 0 { + println!("Stream timeout after receiving {} messages", message_count); + return Ok(()); + } + timeout_count = 0; + } + } + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/rust/examples/stream_recv.rs b/src/rust/examples/stream_recv.rs new file mode 100644 index 0000000..698819a --- /dev/null +++ b/src/rust/examples/stream_recv.rs @@ -0,0 +1,102 @@ +use norm::{Instance, multicast, MulticastExt, EventType, ObjectType, Result}; +use std::env; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + let (address, port) = if args.len() > 2 { + (args[1].as_str(), args[2].parse::().unwrap_or(6003)) + } else { + println!("Using default multicast address 224.1.2.3:6003"); + println!("Usage: {}
", args[0]); + ("224.1.2.3", 6003) + }; + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Set the cache directory for file reception (required for file objects) + instance.set_cache_directory("/tmp/norm")?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 2)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Start the receiver with 1MB buffer + session.start_receiver(1024 * 1024)?; + + println!("NORM stream receiver started on {}:{}", address, port); + println!("Waiting for stream data..."); + + let mut message_count = 0; + let mut buffer = vec![0u8; 4096]; + + // Event loop - wait for data to be received + for event in instance.events() { + match event.event_type { + EventType::RemoteSenderNew => { + println!("New sender connected"); + } + EventType::RxObjectNew => { + let obj_handle = event.object; + let object = norm::Object::from_handle_unowned(obj_handle); + + if object.get_type() == ObjectType::Stream { + println!("New stream object received"); + + // Get info data if available + if object.has_info() { + if let Ok(info) = object.get_info() { + println!("Stream info: {:?}", String::from_utf8_lossy(&info)); + } + } + } + } + EventType::RxObjectUpdated => { + let obj_handle = event.object; + let object = norm::Object::from_handle_unowned(obj_handle); + + if object.get_type() == ObjectType::Stream { + // Try to read data from the stream + match object.stream_read(&mut buffer) { + Ok(bytes_read) if bytes_read > 0 => { + message_count += 1; + let data = &buffer[..bytes_read]; + println!("Message #{}: {:?}", message_count, String::from_utf8_lossy(data)); + + // Check if we've received all expected messages + if message_count >= 5 { + println!("Received all expected messages"); + break; + } + } + Ok(_) => { + // No data available yet + } + Err(e) => { + println!("Error reading from stream: {:?}", e); + } + } + } + } + EventType::RxObjectCompleted => { + println!("Stream object completed"); + break; + } + _ => {} + } + } + + // Clean up + session.stop_receiver(); + + println!("Stream receive example completed"); + + Ok(()) +} diff --git a/src/rust/examples/stream_send.rs b/src/rust/examples/stream_send.rs new file mode 100644 index 0000000..1d5db60 --- /dev/null +++ b/src/rust/examples/stream_send.rs @@ -0,0 +1,92 @@ +use norm::{Instance, multicast, MulticastExt, EventType, Result, FlushMode}; +use std::time::Duration; +use std::{thread, env}; + +fn main() -> Result<()> { + // Parse command line arguments + let args: Vec = env::args().collect(); + let (address, port) = if args.len() > 2 { + (args[1].as_str(), args[2].parse::().unwrap_or(6003)) + } else { + println!("Using default multicast address 224.1.2.3:6003"); + println!("Usage: {}
", args[0]); + ("224.1.2.3", 6003) + }; + + // Create a new NORM instance + let instance = Instance::new(false)?; + + // Create a session with the specified address and port + let session = instance.create_session(address, port, 1)?; + + // Configure multicast settings using our ergonomic API + let mc_config = multicast!(address, port, { + ttl: 64, + loopback: true, + }); + session.with_multicast(&mc_config)?; + + // Set the transmission rate (in bits per second) + session.set_tx_rate(1_000_000.0); + + // Start the sender with 1MB buffer, 1400 byte segments, 64 data segments per block, 16 parity segments + let session_id = rand::random::(); + session.start_sender(session_id, 1024 * 1024, 1400, 64, 16, None)?; + + println!("Opening NORM stream..."); + + // Open a stream with 64KB buffer + let stream = session.stream_open(64 * 1024, Some(b"Example stream"))?; + + // Send multiple messages through the stream + for i in 1..=5 { + let message = format!("Stream message #{}", i); + println!("Sending: {}", message); + + // Write to stream + let bytes_written = stream.stream_write(message.as_bytes())?; + println!("Wrote {} bytes", bytes_written); + + // Mark end of message + stream.stream_mark_eom()?; + + // Small delay between messages + thread::sleep(Duration::from_millis(100)); + } + + println!("Flushing stream..."); + + // Flush the stream with end-of-message marker + stream.stream_flush(true, FlushMode::Active)?; + + // Wait for transmission to complete + let mut tx_complete = false; + for event in instance.events() { + match event.event_type { + EventType::TxObjectSent => { + println!("Stream object sent"); + } + EventType::TxFlushCompleted => { + println!("Stream flush completed"); + tx_complete = true; + } + EventType::TxQueueEmpty => { + if tx_complete { + println!("Transmission queue empty"); + break; + } + } + _ => {} + } + } + + println!("Closing stream gracefully..."); + stream.stream_close(true)?; + + // Clean up + session.stop_sender(); + + println!("Stream send example completed"); + + Ok(()) +} diff --git a/src/rust/norm-sys/Cargo.toml b/src/rust/norm-sys/Cargo.toml new file mode 100644 index 0000000..c82b9ce --- /dev/null +++ b/src/rust/norm-sys/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "norm-sys" +version.workspace = true +edition.workspace = true +links = "norm" +description = "Raw FFI bindings to the NORM (NACK-Oriented Reliable Multicast) library" +license.workspace = true +repository.workspace = true + +[build-dependencies] +bindgen = "0.69" +cc = "1.0" + +[dependencies] +libc = "0.2" + +[features] +default = [] \ No newline at end of file diff --git a/src/rust/norm-sys/build.rs b/src/rust/norm-sys/build.rs new file mode 100644 index 0000000..520b270 --- /dev/null +++ b/src/rust/norm-sys/build.rs @@ -0,0 +1,84 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + // Tell cargo to invalidate the built crate whenever the wrapper.h changes + println!("cargo:rerun-if-changed=wrapper.h"); + + // First, try to find the NORM header directory from an environment variable + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let norm_include_dir = env::var("NORM_INCLUDE_DIR").unwrap_or_else(|_| { + // Default to looking for include files in the main repository directory + format!("{}/../../../include", manifest_dir) + }); + + // Tell cargo to look for libnorm in the build directory + let lib_path = format!("{}/../../../build", manifest_dir); + + println!("cargo:rustc-link-search={}", lib_path); + println!("cargo:rustc-link-lib=norm"); + + // Add rpath so the dynamic library can be found at runtime + // This is especially important for development and testing + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_path); + #[cfg(target_os = "linux")] + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_path); + + // Add linkage to ProtoKit if needed + let protolib_path = format!("{}/protolib", lib_path); + println!("cargo:rustc-link-search={}", protolib_path); + println!("cargo:rustc-link-lib=protokit"); + + // Add rpath for protolib as well + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", protolib_path); + #[cfg(target_os = "linux")] + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", protolib_path); + + // Link pthread on Unix + if cfg!(unix) { + println!("cargo:rustc-link-lib=pthread"); + } + + // On macOS, we might need additional system libraries + if cfg!(target_os = "macos") { + println!("cargo:rustc-link-lib=resolv"); + } + + // On Solaris, add these libraries + if cfg!(target_os = "solaris") { + println!("cargo:rustc-link-lib=nsl"); + println!("cargo:rustc-link-lib=socket"); + println!("cargo:rustc-link-lib=resolv"); + } + + // Generate bindings for the NORM API + let bindings = bindgen::Builder::default() + .header("wrapper.h") + // IMPORTANT: Tell clang to treat this as C++ code + .clang_arg("-x") + .clang_arg("c++") + .clang_arg(format!("-I{}", norm_include_dir)) + // Whitelist NORM functions, types, and constants + .allowlist_function("Norm.*") + .allowlist_type("Norm.*") + .allowlist_var("NORM_.*") + // Make the generated bindings derive common traits + .derive_default(true) + .derive_debug(true) + .derive_copy(true) + .derive_eq(true) + // Generate documentation from C comments + .generate_comments(true) + // Parse callbacks for cargo build info (updated to new API) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Unable to generate NORM bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write NORM bindings!"); +} \ No newline at end of file diff --git a/src/rust/norm-sys/src/lib.rs b/src/rust/norm-sys/src/lib.rs new file mode 100644 index 0000000..76ad58e --- /dev/null +++ b/src/rust/norm-sys/src/lib.rs @@ -0,0 +1,24 @@ +//! Raw FFI bindings to the NORM (NACK-Oriented Reliable Multicast) library. +//! +//! This crate provides raw FFI bindings to the NORM C API, generated using bindgen. +//! It is not intended to be used directly, but rather through the `norm` crate which +//! provides safe, idiomatic Rust wrappers. + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(clippy::all)] + +// The bindings will be included here by the build script +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_constants() { + // Verify that we can access the version constants + assert!(NORM_VERSION_MAJOR > 0); + } +} \ No newline at end of file diff --git a/src/rust/norm-sys/wrapper.h b/src/rust/norm-sys/wrapper.h new file mode 100644 index 0000000..63ea6b6 --- /dev/null +++ b/src/rust/norm-sys/wrapper.h @@ -0,0 +1,4 @@ +/* Wrapper header for bindgen to generate bindings for NORM API */ + +/* Include the main NORM API header */ +#include "normApi.h" \ No newline at end of file diff --git a/src/rust/norm/Cargo.toml b/src/rust/norm/Cargo.toml new file mode 100644 index 0000000..1e316b2 --- /dev/null +++ b/src/rust/norm/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "norm" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Safe Rust bindings for NORM (NACK-Oriented Reliable Multicast)" + +[dependencies] +norm-sys = { workspace = true } +libc = "0.2" +thiserror = "1.0" +tokio = { version = "1.0", features = ["full"], optional = true } +rand = { workspace = true } + +[dev-dependencies] + +[features] +default = [] +tokio = ["dep:tokio"] +async-std = [] + +[[example]] +name = "data_send" +path = "../examples/data_send.rs" + +[[example]] +name = "data_recv" +path = "../examples/data_recv.rs" + +[[example]] +name = "stream_send" +path = "../examples/stream_send.rs" + +[[example]] +name = "stream_recv" +path = "../examples/stream_recv.rs" \ No newline at end of file diff --git a/src/rust/norm/build.rs b/src/rust/norm/build.rs new file mode 100644 index 0000000..06a9aa4 --- /dev/null +++ b/src/rust/norm/build.rs @@ -0,0 +1,7 @@ +fn main() { + // The norm-sys crate already handles linking to libnorm + // This build script is included for future expansion if needed + + // We might want to add conditional compilation flags, feature detection, etc. + println!("cargo:rerun-if-changed=build.rs"); +} \ No newline at end of file diff --git a/src/rust/norm/src/error.rs b/src/rust/norm/src/error.rs new file mode 100644 index 0000000..625e84f --- /dev/null +++ b/src/rust/norm/src/error.rs @@ -0,0 +1,187 @@ +use std::fmt; +use std::error::Error as StdError; + +/// Error type for NORM operations +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + /// An invalid handle was provided to a NORM function + InvalidHandle, + + /// An invalid network address was provided + InvalidAddress, + + /// An invalid parameter was provided to a NORM function + InvalidParameter, + + /// Memory allocation failed + AllocationFailed, + + /// A socket-related error occurred + SocketError(String), + + /// A file-related error occurred + FileError(String), + + /// An operation timed out + Timeout, + + /// A resource was not ready for the requested operation + NotReady, + + /// An operation failed for an unspecified reason + OperationFailed(String), + + /// A system error occurred (with errno) + #[cfg(unix)] + SystemError { + /// The error message + message: String, + /// The errno value + errno: i32, + }, + + /// A system error occurred (with Win32 error code) + #[cfg(windows)] + SystemError { + /// The error message + message: String, + /// The Win32 error code + error_code: u32, + }, + + /// A string conversion error occurred + StringConversionError, + + /// A null pointer was encountered where a valid pointer was expected + NullPointer, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::InvalidHandle => write!(f, "Invalid NORM handle"), + Error::InvalidAddress => write!(f, "Invalid network address"), + Error::InvalidParameter => write!(f, "Invalid parameter"), + Error::AllocationFailed => write!(f, "Memory allocation failed"), + Error::SocketError(s) => write!(f, "Socket error: {}", s), + Error::FileError(s) => write!(f, "File error: {}", s), + Error::Timeout => write!(f, "Operation timed out"), + Error::NotReady => write!(f, "Resource not ready"), + Error::OperationFailed(s) => write!(f, "Operation failed: {}", s), + #[cfg(unix)] + Error::SystemError { message, errno } => { + write!(f, "System error: {} (errno: {})", message, errno) + } + #[cfg(windows)] + Error::SystemError { message, error_code } => { + write!(f, "System error: {} (error code: {})", message, error_code) + } + Error::StringConversionError => write!(f, "String conversion error"), + Error::NullPointer => write!(f, "Null pointer encountered"), + } + } +} + +impl StdError for Error {} + +/// Result type for NORM operations +pub type Result = std::result::Result; + +/// Helper function to convert a C string to a Rust string result +pub(crate) fn c_string_to_string(c_str: *const libc::c_char) -> Result { + if c_str.is_null() { + return Err(Error::NullPointer); + } + + unsafe { + let c_str = std::ffi::CStr::from_ptr(c_str); + c_str.to_str() + .map(|s| s.to_owned()) + .map_err(|_| Error::StringConversionError) + } +} + +/// Helper function to convert a Rust string to a C string result +pub(crate) fn string_to_c_string(s: &str) -> Result { + std::ffi::CString::new(s) + .map_err(|_| Error::StringConversionError) +} + +/// Convert a boolean result from NORM API to a Result +pub(crate) fn bool_result(success: bool, error_message: &str) -> Result<()> { + if success { + Ok(()) + } else { + Err(Error::OperationFailed(error_message.to_string())) + } +} + +/// Helper to check if a handle is valid +/// +/// # Safety +/// +/// This function is unsafe because it may compare with extern static values +/// that are not controlled by the Rust type system. +pub(crate) unsafe fn check_handle(handle: T, invalid: T) -> Result +where + T: PartialEq +{ + if handle == invalid { + Err(Error::InvalidHandle) + } else { + Ok(handle) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bool_result_success() { + assert!(bool_result(true, "Test error").is_ok()); + } + + #[test] + fn test_bool_result_failure() { + let result = bool_result(false, "Test error"); + assert!(result.is_err()); + match result { + Err(Error::OperationFailed(msg)) => assert_eq!(msg, "Test error"), + _ => panic!("Expected OperationFailed error"), + } + } + + #[test] + fn test_string_to_c_string() { + let result = string_to_c_string("test"); + assert!(result.is_ok()); + + let c_str = result.unwrap(); + assert_eq!(c_str.to_str().unwrap(), "test"); + } + + #[test] + fn test_string_to_c_string_with_null() { + // Build a string with an embedded null byte without a literal null in source + let s = format!("test{}embedded", '\0'); + let result = string_to_c_string(&s); + assert!(result.is_err()); + match result { + Err(Error::StringConversionError) => {}, + _ => panic!("Expected StringConversionError"), + } + } + + #[test] + fn test_error_display() { + let err = Error::InvalidHandle; + assert_eq!(err.to_string(), "Invalid NORM handle"); + + let err = Error::OperationFailed("test failure".to_string()); + assert_eq!(err.to_string(), "Operation failed: test failure"); + + let err = Error::FileError("file not found".to_string()); + assert_eq!(err.to_string(), "File error: file not found"); + } +} + diff --git a/src/rust/norm/src/event.rs b/src/rust/norm/src/event.rs new file mode 100644 index 0000000..6ff6e87 --- /dev/null +++ b/src/rust/norm/src/event.rs @@ -0,0 +1,53 @@ +use crate::types::EventType; +use norm_sys::*; + +/// A NORM event +/// +/// This struct represents an event from the NORM protocol. It wraps the raw +/// C event struct and provides accessors for the event data. +/// +/// Events are generated by the NORM protocol and can be retrieved using +/// `Instance::next_event` or `Instance::events`. +#[derive(Debug, Clone)] +pub struct Event { + /// The type of event + pub event_type: EventType, + /// The session handle + pub session: NormSessionHandle, + /// The sender handle + pub sender: NormNodeHandle, + /// The object handle + pub object: NormObjectHandle, +} + +impl Event { + /// Create a new event from a raw NORM event + /// + /// # Arguments + /// * `raw_event` - The raw NORM event from the C API + /// + /// # Returns + /// A new event struct + pub(crate) fn from_raw(raw_event: NormEvent) -> Self { + Event { + event_type: EventType::from(raw_event.type_), + session: raw_event.session, + sender: raw_event.sender, + object: raw_event.object, + } + } + + /// Check if this event is a specific event type + /// + /// # Arguments + /// * `event_type` - The event type to check for + /// + /// # Returns + /// `true` if the event is of the specified type, `false` otherwise + pub fn is(&self, event_type: EventType) -> bool { + self.event_type == event_type + } +} + +// This will be expanded later to provide additional helper methods and convenience functions +// for working with different event types \ No newline at end of file diff --git a/src/rust/norm/src/instance.rs b/src/rust/norm/src/instance.rs new file mode 100644 index 0000000..efe9023 --- /dev/null +++ b/src/rust/norm/src/instance.rs @@ -0,0 +1,217 @@ +use crate::error::{Result, bool_result, check_handle, string_to_c_string}; +use crate::types::*; +use crate::event::Event; +use crate::session::Session; +use norm_sys::*; +use std::mem; + +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, RawFd}; + +/// NORM instance handle with RAII semantics. +/// +/// This is the top-level object for interacting with the NORM library. It provides +/// functionality for creating sessions, handling events, and configuring global options. +/// +/// When dropped, it will automatically clean up resources by calling `NormDestroyInstance`. +#[derive(Debug)] +pub struct Instance { + /// The raw NORM instance handle + handle: NormInstanceHandle, +} + +impl Instance { + /// Create a new NORM instance + /// + /// # Arguments + /// * `priority_boost` - Whether to boost the priority of the NORM thread + /// + /// # Returns + /// A new NORM instance or an error if the instance could not be created + pub fn new(priority_boost: bool) -> Result { + let handle = unsafe { NormCreateInstance(priority_boost) }; + unsafe { check_handle(handle, NORM_INSTANCE_INVALID)? }; + Ok(Instance { handle }) + } + + /// Stop the NORM instance + /// + /// This stops any running NORM threads but doesn't destroy the instance. + pub fn stop(&self) { + unsafe { NormStopInstance(self.handle) }; + } + + /// Restart the NORM instance + /// + /// # Returns + /// `Ok(())` if the instance was successfully restarted, `Err` otherwise + pub fn restart(&self) -> Result<()> { + let success = unsafe { NormRestartInstance(self.handle) }; + bool_result(success, "Failed to restart NORM instance") + } + + /// Suspend the NORM instance + /// + /// # Returns + /// `Ok(())` if the instance was successfully suspended, `Err` otherwise + pub fn suspend(&self) -> Result<()> { + let success = unsafe { NormSuspendInstance(self.handle) }; + bool_result(success, "Failed to suspend NORM instance") + } + + /// Resume a suspended NORM instance + pub fn resume(&self) { + unsafe { NormResumeInstance(self.handle) }; + } + + /// Set the cache directory for file reception + /// + /// This MUST be set to enable NORM_OBJECT_FILE reception! + /// + /// # Arguments + /// * `cache_path` - The path to the cache directory + /// + /// # Returns + /// `Ok(())` if the cache directory was successfully set, `Err` otherwise + pub fn set_cache_directory>(&self, cache_path: P) -> Result<()> { + let c_path = string_to_c_string(cache_path.as_ref())?; + let success = unsafe { NormSetCacheDirectory(self.handle, c_path.as_ptr()) }; + bool_result(success, "Failed to set cache directory") + } + + /// Get the next NORM event + /// + /// # Arguments + /// * `wait` - Whether to wait for an event or return immediately if none is available + /// + /// # Returns + /// `Ok(Some(Event))` if an event was available, `Ok(None)` if no event was available + /// and `wait` was `false`, or `Err` if an error occurred + pub fn next_event(&self, wait: bool) -> Result> { + let mut raw_event = unsafe { mem::zeroed::() }; + let success = unsafe { NormGetNextEvent(self.handle, &mut raw_event, wait) }; + + if !success { + return Ok(None); + } + + Ok(Some(Event::from_raw(raw_event))) + } + + /// Create an iterator over NORM events + /// + /// This is a convenience wrapper around `next_event` that returns an iterator + /// that will continuously yield events. The iterator will block until an event + /// is available. + /// + /// # Returns + /// An iterator over NORM events + pub fn events(&self) -> EventIterator<'_> { + EventIterator { instance: self } + } + + /// Create a new NORM session + /// + /// # Arguments + /// * `session_address` - The multicast address for the session + /// * `session_port` - The port number for the session + /// * `local_node_id` - The local node ID (use `NORM_NODE_ANY` for automatic) + /// + /// # Returns + /// A new NORM session or an error if the session could not be created + pub fn create_session>( + &self, + session_address: A, + session_port: u16, + local_node_id: NodeId, + ) -> Result { + Session::new(self.handle, session_address, session_port, local_node_id) + } + + /// Get the file descriptor for the NORM instance + /// + /// This descriptor can be used with `select()` or similar APIs to wait for + /// NORM events. + /// + /// # Returns + /// The file descriptor for the NORM instance + #[cfg(unix)] + pub fn descriptor(&self) -> RawFd { + unsafe { NormGetDescriptor(self.handle) } + } + + /// Open a debug log file + /// + /// # Arguments + /// * `path` - The path to the log file + /// + /// # Returns + /// `Ok(())` if the log file was successfully opened, `Err` otherwise + pub fn open_debug_log>(&self, path: P) -> Result<()> { + let c_path = string_to_c_string(path.as_ref())?; + let success = unsafe { NormOpenDebugLog(self.handle, c_path.as_ptr()) }; + bool_result(success, "Failed to open debug log file") + } + + /// Close the debug log file + pub fn close_debug_log(&self) { + unsafe { NormCloseDebugLog(self.handle) }; + } + + /// Set custom allocation functions for NORM_OBJECT_DATA + /// + /// # Arguments + /// * `alloc_func` - Function to allocate memory for NORM_OBJECT_DATA + /// * `free_func` - Function to free memory allocated by `alloc_func` + /// + /// # Safety + /// This function is unsafe because it takes raw function pointers that + /// must adhere to the expected memory allocation/deallocation semantics. + pub unsafe fn set_allocation_functions( + &self, + alloc_func: NormAllocFunctionHandle, + free_func: NormFreeFunctionHandle, + ) { + NormSetAllocationFunctions(self.handle, alloc_func, free_func); + } + + /// Get the raw NORM instance handle + /// + /// # Returns + /// The raw NORM instance handle + pub(crate) fn handle(&self) -> NormInstanceHandle { + self.handle + } +} + +impl Drop for Instance { + fn drop(&mut self) { + unsafe { NormDestroyInstance(self.handle) }; + } +} + +#[cfg(unix)] +impl AsRawFd for Instance { + fn as_raw_fd(&self) -> RawFd { + self.descriptor() + } +} + +/// Iterator over NORM events +/// +/// This iterator will continuously yield events from a NORM instance. +/// It will block until an event is available. +pub struct EventIterator<'a> { + instance: &'a Instance, +} + +impl<'a> Iterator for EventIterator<'a> { + type Item = Event; + + fn next(&mut self) -> Option { + match self.instance.next_event(true) { + Ok(Some(event)) => Some(event), + _ => None, + } + } +} \ No newline at end of file diff --git a/src/rust/norm/src/lib.rs b/src/rust/norm/src/lib.rs new file mode 100644 index 0000000..ebaee36 --- /dev/null +++ b/src/rust/norm/src/lib.rs @@ -0,0 +1,51 @@ +//! Safe, idiomatic Rust bindings for the NORM (NACK-Oriented Reliable Multicast) protocol. +//! +//! NORM is a protocol for reliable multicast transmission developed by the US Naval Research +//! Laboratory. This crate provides safe Rust wrappers around the C API, handling resource +//! management and providing an idiomatic interface. +//! +//! # Features +//! +//! - RAII-based resource management for NORM handles +//! - Idiomatic error handling +//! - Iterator-based event handling +//! - Ergonomic multicast configuration +//! - Optional async support with tokio (feature = "tokio") + +// Re-export norm-sys for advanced users +pub use norm_sys; + +// Module declarations +mod error; +mod types; +mod instance; +mod session; +mod object; +mod node; +mod event; +mod multicast; + +// Optional modules +// Note: Async support with tokio is planned for future implementation +// #[cfg(feature = "tokio")] +// pub mod tokio; + +// Public re-exports +pub use error::{Error, Result}; +pub use types::*; +pub use instance::Instance; +pub use session::Session; +pub use object::Object; +pub use node::Node; +pub use event::Event; +pub use multicast::{MulticastConfig, MulticastExt, is_multicast_address}; + +// Version information +pub const VERSION_MAJOR: u32 = norm_sys::NORM_VERSION_MAJOR as u32; +pub const VERSION_MINOR: u32 = norm_sys::NORM_VERSION_MINOR as u32; +pub const VERSION_PATCH: u32 = norm_sys::NORM_VERSION_PATCH as u32; + +/// Get the version of the NORM library as a tuple (major, minor, patch) +pub fn version() -> (u32, u32, u32) { + (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) +} \ No newline at end of file diff --git a/src/rust/norm/src/multicast.rs b/src/rust/norm/src/multicast.rs new file mode 100644 index 0000000..e6e9092 --- /dev/null +++ b/src/rust/norm/src/multicast.rs @@ -0,0 +1,339 @@ +use crate::error::Result; +use crate::session::Session; +use std::net::IpAddr; +use std::fmt; +use std::str::FromStr; + +/// Ergonomic multicast configuration for NORM sessions. +/// +/// This struct provides a builder-style API for configuring multicast options +/// for a NORM session. It handles the details of setting up the various multicast +/// options and applying them to a session. +#[derive(Debug, Clone)] +pub struct MulticastConfig { + /// The multicast address + address: String, + /// The port + port: u16, + /// The network interface to use for multicast + interface: Option, + /// The time-to-live (TTL) for multicast packets + ttl: Option, + /// Whether to enable loopback + loopback: Option, + /// The source address for SSM (Source-Specific Multicast) + ssm_source: Option, + /// The type of service (TOS) value for IP packets + tos: Option, +} + +impl MulticastConfig { + /// Create a new multicast configuration + /// + /// # Arguments + /// * `address` - The multicast address + /// * `port` - The port number + /// + /// # Returns + /// A new multicast configuration + pub fn new(address: impl Into, port: u16) -> Self { + Self { + address: address.into(), + port, + interface: None, + ttl: None, + loopback: None, + ssm_source: None, + tos: None, + } + } + + /// Set the network interface for multicast + /// + /// # Arguments + /// * `interface` - The name of the interface to use + /// + /// # Returns + /// Self for method chaining + pub fn interface(mut self, interface: impl Into) -> Self { + self.interface = Some(interface.into()); + self + } + + /// Set the time-to-live (TTL) for multicast packets + /// + /// # Arguments + /// * `ttl` - The TTL value (0-255) + /// + /// # Returns + /// Self for method chaining + pub fn ttl(mut self, ttl: u8) -> Self { + self.ttl = Some(ttl); + self + } + + /// Enable or disable multicast loopback + /// + /// # Arguments + /// * `enable` - Whether to enable loopback + /// + /// # Returns + /// Self for method chaining + pub fn loopback(mut self, enable: bool) -> Self { + self.loopback = Some(enable); + self + } + + /// Set the source address for SSM (Source-Specific Multicast) + /// + /// # Arguments + /// * `source` - The source address + /// + /// # Returns + /// Self for method chaining + pub fn ssm_source(mut self, source: impl Into) -> Self { + self.ssm_source = Some(source.into()); + self + } + + /// Set the type of service (TOS) value for IP packets + /// + /// # Arguments + /// * `tos` - The TOS value (0-255) + /// + /// # Returns + /// Self for method chaining + pub fn tos(mut self, tos: u8) -> Self { + self.tos = Some(tos); + self + } + + /// Apply the configuration to a session + /// + /// # Arguments + /// * `session` - The session to apply the configuration to + /// + /// # Returns + /// `Ok(())` on success or an `Err` if any configuration option could not be applied + pub fn apply(&self, session: &Session) -> Result<()> { + if let Some(ref interface) = self.interface { + session.set_multicast_interface(interface)?; + } + + if let Some(ttl) = self.ttl { + session.set_ttl(ttl)?; + } + + if let Some(loopback) = self.loopback { + session.set_multicast_loopback(loopback)?; + } + + if let Some(ref source) = self.ssm_source { + session.set_ssm(source)?; + } + + if let Some(tos) = self.tos { + session.set_tos(tos)?; + } + + Ok(()) + } + + /// Get the multicast address + /// + /// # Returns + /// The multicast address as a string + pub fn address(&self) -> &str { + &self.address + } + + /// Get the port number + /// + /// # Returns + /// The port number + pub fn port(&self) -> u16 { + self.port + } +} + +impl fmt::Display for MulticastConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.address, self.port)?; + + if let Some(ref interface) = self.interface { + write!(f, " on {}", interface)?; + } + + if let Some(ttl) = self.ttl { + write!(f, " ttl={}", ttl)?; + } + + if let Some(loopback) = self.loopback { + write!(f, " loopback={}", loopback)?; + } + + if let Some(ref source) = self.ssm_source { + write!(f, " ssm_source={}", source)?; + } + + if let Some(tos) = self.tos { + write!(f, " tos={}", tos)?; + } + + Ok(()) + } +} + +/// Convenient macro for multicast configuration +/// +/// # Examples +/// +/// ``` +/// # use norm::multicast; +/// let config = multicast!("224.1.2.3", 6003, { +/// ttl: 64, +/// interface: "eth0", +/// loopback: true, +/// }); +/// ``` +#[macro_export] +macro_rules! multicast { + ($addr:expr, $port:expr) => { + $crate::MulticastConfig::new($addr, $port) + }; + ($addr:expr, $port:expr, { $($key:ident: $value:expr),* $(,)? }) => {{ + let mut config = $crate::MulticastConfig::new($addr, $port); + $( + config = config.$key($value); + )* + config + }}; +} + +/// Extension trait for NORM sessions to apply multicast configurations +pub trait MulticastExt { + /// Apply a multicast configuration to the session + /// + /// # Arguments + /// * `config` - The multicast configuration to apply + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the configuration could not be applied + fn with_multicast(&self, config: &MulticastConfig) -> Result<&Self>; +} + +impl MulticastExt for Session { + fn with_multicast(&self, config: &MulticastConfig) -> Result<&Self> { + config.apply(self)?; + Ok(self) + } +} + +/// Check if an IP address is a multicast address +/// +/// # Arguments +/// * `addr` - The IP address to check +/// +/// # Returns +/// `true` if the address is a multicast address, `false` otherwise +pub fn is_multicast_address(addr: &str) -> bool { + if let Ok(ip) = IpAddr::from_str(addr) { + match ip { + IpAddr::V4(ipv4) => ipv4.is_multicast(), + IpAddr::V6(ipv6) => ipv6.is_multicast(), + } + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multicast_config_builder() { + let config = MulticastConfig::new("224.1.2.3", 6003) + .ttl(64) + .interface("eth0") + .loopback(true) + .ssm_source("192.168.1.1") + .tos(0x10); + + assert_eq!(config.address(), "224.1.2.3"); + assert_eq!(config.port(), 6003); + assert_eq!(config.ttl, Some(64)); + assert_eq!(config.interface.as_deref(), Some("eth0")); + assert_eq!(config.loopback, Some(true)); + assert_eq!(config.ssm_source.as_deref(), Some("192.168.1.1")); + assert_eq!(config.tos, Some(0x10)); + } + + #[test] + fn test_multicast_config_display() { + let config = MulticastConfig::new("224.1.2.3", 6003) + .ttl(64) + .loopback(true); + + let display = format!("{}", config); + assert!(display.contains("224.1.2.3:6003")); + assert!(display.contains("ttl=64")); + assert!(display.contains("loopback=true")); + } + + #[test] + fn test_is_multicast_address_ipv4() { + // Valid IPv4 multicast addresses (224.0.0.0 to 239.255.255.255) + assert!(is_multicast_address("224.0.0.0")); + assert!(is_multicast_address("224.1.2.3")); + assert!(is_multicast_address("239.255.255.255")); + + // Invalid IPv4 multicast addresses + assert!(!is_multicast_address("192.168.1.1")); + assert!(!is_multicast_address("10.0.0.1")); + assert!(!is_multicast_address("127.0.0.1")); + } + + #[test] + fn test_is_multicast_address_ipv6() { + // Valid IPv6 multicast addresses (start with ff00::/8) + assert!(is_multicast_address("ff02::1")); + assert!(is_multicast_address("ff05::1:3")); + + // Invalid IPv6 multicast addresses + assert!(!is_multicast_address("fe80::1")); + assert!(!is_multicast_address("::1")); + assert!(!is_multicast_address("2001:db8::1")); + } + + #[test] + fn test_is_multicast_address_invalid() { + // Invalid IP addresses + assert!(!is_multicast_address("not.an.ip")); + assert!(!is_multicast_address("")); + assert!(!is_multicast_address("999.999.999.999")); + } + + #[test] + fn test_multicast_macro() { + let config = multicast!("224.1.2.3", 6003, { + ttl: 64, + loopback: true, + }); + + assert_eq!(config.address(), "224.1.2.3"); + assert_eq!(config.port(), 6003); + assert_eq!(config.ttl, Some(64)); + assert_eq!(config.loopback, Some(true)); + } + + #[test] + fn test_multicast_macro_simple() { + let config = multicast!("224.1.2.3", 6003); + + assert_eq!(config.address(), "224.1.2.3"); + assert_eq!(config.port(), 6003); + assert_eq!(config.ttl, None); + assert_eq!(config.interface, None); + } +} \ No newline at end of file diff --git a/src/rust/norm/src/node.rs b/src/rust/norm/src/node.rs new file mode 100644 index 0000000..f970ddc --- /dev/null +++ b/src/rust/norm/src/node.rs @@ -0,0 +1,77 @@ +use crate::types::*; +use norm_sys::*; + +/// NORM node handle with RAII semantics. +/// +/// A Node represents a participant in a NORM session, either as a sender or receiver. +/// +/// When dropped, the node will be released by calling `NormNodeRelease`. +#[derive(Debug)] +pub struct Node { + /// The raw NORM node handle + handle: NormNodeHandle, + /// Whether this node is owned by us + owned: bool, +} + +impl Node { + /// Create a new Node from a raw NORM node handle + /// + /// # Arguments + /// * `handle` - The raw NORM node handle + /// + /// # Returns + /// A new Node + pub(crate) fn from_handle(handle: NormNodeHandle) -> Self { + Node { handle, owned: true } + } + + /// Create a new Node from a raw NORM node handle, without taking ownership + /// + /// # Arguments + /// * `handle` - The raw NORM node handle + /// + /// # Returns + /// A new Node that will not release the handle when dropped + pub fn from_handle_unowned(handle: NormNodeHandle) -> Self { + Node { handle, owned: false } + } + + /// Get the ID of the node + /// + /// # Returns + /// The node ID + pub fn id(&self) -> NodeId { + unsafe { NormNodeGetId(self.handle) } + } + + /// Get the raw NORM node handle + /// + /// # Returns + /// The raw NORM node handle + pub(crate) fn handle(&self) -> NormNodeHandle { + self.handle + } + + /// Retain a node handle, incrementing its reference count + pub fn retain(&self) { + unsafe { NormNodeRetain(self.handle) }; + } + + /// Explicitly release a node handle, decrementing its reference count + /// + /// This is called automatically when the node is dropped, if it is owned. + pub fn release(&self) { + unsafe { NormNodeRelease(self.handle) }; + } +} + +impl Drop for Node { + fn drop(&mut self) { + if self.owned { + unsafe { NormNodeRelease(self.handle) }; + } + } +} + +// More methods will be added later \ No newline at end of file diff --git a/src/rust/norm/src/object.rs b/src/rust/norm/src/object.rs new file mode 100644 index 0000000..a533497 --- /dev/null +++ b/src/rust/norm/src/object.rs @@ -0,0 +1,316 @@ +use crate::error::{Error, Result, check_handle}; +use crate::types::*; +use crate::node::Node; +use norm_sys::*; +use std::slice; +use std::os::raw::c_char; + +/// NORM object handle with RAII semantics. +/// +/// A Object represents a NORM transmission object, which can be a file, data, or stream. +/// +/// When dropped, the object will be released by calling `NormObjectRelease`. +#[derive(Debug)] +pub struct Object { + /// The raw NORM object handle + handle: NormObjectHandle, + /// Whether this object is owned by us + owned: bool, +} + +impl Object { + /// Create a new Object from a raw NORM object handle + /// + /// # Arguments + /// * `handle` - The raw NORM object handle + /// + /// # Returns + /// A new Object + pub(crate) fn from_handle(handle: NormObjectHandle) -> Self { + Object { handle, owned: true } + } + + /// Create a new Object from a raw NORM object handle, without taking ownership + /// + /// # Arguments + /// * `handle` - The raw NORM object handle + /// + /// # Returns + /// A new Object that will not release the handle when dropped + pub fn from_handle_unowned(handle: NormObjectHandle) -> Self { + Object { handle, owned: false } + } + + /// Get the type of the object + /// + /// # Returns + /// The object type + pub fn get_type(&self) -> ObjectType { + let obj_type = unsafe { NormObjectGetType(self.handle) }; + ObjectType::from(obj_type) + } + + /// Check if the object has info data + /// + /// # Returns + /// `true` if the object has info data, `false` otherwise + pub fn has_info(&self) -> bool { + unsafe { NormObjectHasInfo(self.handle) } + } + + /// Get the info data associated with the object + /// + /// # Returns + /// The info data as a byte vector, or an error if the info data could not be retrieved + pub fn get_info(&self) -> Result> { + if !self.has_info() { + return Ok(Vec::new()); + } + + let info_len = unsafe { NormObjectGetInfoLength(self.handle) }; + if info_len == 0 { + return Ok(Vec::new()); + } + + let mut buffer = vec![0u8; info_len as usize]; + let bytes_read = unsafe { + NormObjectGetInfo( + self.handle, + buffer.as_mut_ptr() as *mut c_char, + info_len, + ) + }; + + if bytes_read == 0 { + Err(Error::OperationFailed("Failed to get info data".to_string())) + } else { + buffer.truncate(bytes_read as usize); + Ok(buffer) + } + } + + /// Get the size of the object in bytes + /// + /// # Returns + /// The size of the object in bytes + pub fn size(&self) -> Size { + unsafe { NormObjectGetSize(self.handle) } + } + + /// Get the number of bytes still pending transmission + /// + /// # Returns + /// The number of bytes still pending transmission + pub fn bytes_pending(&self) -> Size { + unsafe { NormObjectGetBytesPending(self.handle) } + } + + /// Access the data of a NORM_OBJECT_DATA object + /// + /// # Returns + /// A slice containing the data, or an error if the data could not be accessed + pub fn access_data(&self) -> Result<&[u8]> { + if self.get_type() != ObjectType::Data { + return Err(Error::InvalidParameter); + } + + let data_ptr = unsafe { NormDataAccessData(self.handle) }; + if data_ptr.is_null() { + return Err(Error::NullPointer); + } + + let size = self.size() as usize; + let data_slice = unsafe { slice::from_raw_parts(data_ptr as *const u8, size) }; + Ok(data_slice) + } + + /// Cancel the object + /// + /// This aborts transmission or reception of the object. + pub fn cancel(&self) { + unsafe { NormObjectCancel(self.handle) }; + } + + /// Get the sender of the object + /// + /// # Returns + /// The sender node, or an error if the sender could not be retrieved + pub fn get_sender(&self) -> Result { + let sender = unsafe { NormObjectGetSender(self.handle) }; + unsafe { check_handle(sender, NORM_NODE_INVALID) } + .map(Node::from_handle_unowned) + } + + /// Get the raw NORM object handle + /// + /// # Returns + /// The raw NORM object handle + pub(crate) fn handle(&self) -> NormObjectHandle { + self.handle + } + + /// Retain an object handle, incrementing its reference count + pub fn retain(&self) { + unsafe { NormObjectRetain(self.handle) }; + } + + /// Explicitly release an object handle, decrementing its reference count + /// + /// This is called automatically when the object is dropped, if it is owned. + pub fn release(&self) { + unsafe { NormObjectRelease(self.handle) }; + } + + // Stream-specific operations + + /// Write data to a stream object + /// + /// # Arguments + /// * `data` - The data to write to the stream + /// + /// # Returns + /// The number of bytes written, or an error if the operation failed + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_write(&self, data: &[u8]) -> Result { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + let bytes_written = unsafe { + NormStreamWrite( + self.handle, + data.as_ptr() as *const c_char, + data.len() as u32, + ) + }; + + Ok(bytes_written as usize) + } + + /// Flush a stream object + /// + /// # Arguments + /// * `eom` - Whether to mark this as end-of-message + /// * `flush_mode` - The flush mode to use + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_flush(&self, eom: bool, flush_mode: crate::types::FlushMode) -> Result<()> { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + unsafe { + NormStreamFlush( + self.handle, + eom, + flush_mode.into(), + ); + } + + Ok(()) + } + + /// Mark end-of-message on a stream object + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_mark_eom(&self) -> Result<()> { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + unsafe { NormStreamMarkEom(self.handle) }; + Ok(()) + } + + /// Close a stream object + /// + /// # Arguments + /// * `graceful` - Whether to close gracefully (wait for pending data) or immediately + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_close(&self, graceful: bool) -> Result<()> { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + unsafe { NormStreamClose(self.handle, graceful) }; + Ok(()) + } + + /// Read data from a stream object + /// + /// # Arguments + /// * `buffer` - The buffer to read data into + /// + /// # Returns + /// A tuple of (bytes_read, data_slice) or an error if the operation failed + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + /// Returns `Error::OperationFailed` if the read operation failed + pub fn stream_read(&self, buffer: &mut [u8]) -> Result { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + let mut num_bytes = buffer.len() as u32; + let success = unsafe { + NormStreamRead( + self.handle, + buffer.as_mut_ptr() as *mut c_char, + &mut num_bytes, + ) + }; + + if success { + Ok(num_bytes as usize) + } else { + Err(Error::OperationFailed("Failed to read from stream".to_string())) + } + } + + /// Check if a stream has available space for writing + /// + /// # Returns + /// `true` if the stream has vacancy for writing, `false` otherwise + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_has_vacancy(&self) -> Result { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + let has_vacancy = unsafe { NormStreamHasVacancy(self.handle) }; + Ok(has_vacancy) + } + + /// Seek to the start of the next message in a stream + /// + /// # Returns + /// `true` if a message start was found, `false` otherwise + /// + /// # Errors + /// Returns `Error::InvalidParameter` if this is not a stream object + pub fn stream_seek_msg_start(&self) -> Result { + if self.get_type() != ObjectType::Stream { + return Err(Error::InvalidParameter); + } + + let found = unsafe { NormStreamSeekMsgStart(self.handle) }; + Ok(found) + } +} + +impl Drop for Object { + fn drop(&mut self) { + if self.owned { + unsafe { NormObjectRelease(self.handle) }; + } + } +} \ No newline at end of file diff --git a/src/rust/norm/src/session.rs b/src/rust/norm/src/session.rs new file mode 100644 index 0000000..2b5cc0b --- /dev/null +++ b/src/rust/norm/src/session.rs @@ -0,0 +1,459 @@ +use crate::error::{Error, Result, bool_result, check_handle, string_to_c_string}; +use crate::types::*; +use crate::object::Object; +use norm_sys::*; +use std::os::raw::c_char; +use std::ptr; + +/// NORM session handle with RAII semantics. +/// +/// A Session represents a NORM protocol session, which can be used to send and receive data. +/// Sessions can operate in multicast or unicast mode, and can act as a sender, receiver, or both. +/// +/// When dropped, the session will automatically clean up resources by calling `NormDestroySession`. +#[derive(Debug)] +pub struct Session { + /// The raw NORM session handle + handle: NormSessionHandle, +} + +impl Session { + /// Create a new NORM session. + /// + /// This is typically called through `Instance::create_session` rather than directly. + /// + /// # Arguments + /// * `instance` - The NORM instance to create the session on + /// * `session_address` - The multicast or unicast address for the session + /// * `session_port` - The port number for the session + /// * `local_node_id` - The local node ID (use `NORM_NODE_ANY` for automatic) + /// + /// # Returns + /// A new NORM session or an error if the session could not be created + pub(crate) fn new>( + instance: NormInstanceHandle, + session_address: A, + session_port: u16, + local_node_id: NodeId, + ) -> Result { + let c_addr = string_to_c_string(session_address.as_ref())?; + let handle = unsafe { + NormCreateSession(instance, c_addr.as_ptr(), session_port, local_node_id) + }; + + unsafe { check_handle(handle, NORM_SESSION_INVALID)? }; + Ok(Session { handle }) + } + + /// Start the session as a NORM sender + /// + /// # Arguments + /// * `instance_id` - The instance ID (session identifier) + /// * `buffer_space` - Buffer size in bytes for the sender's transmission queue + /// * `segment_size` - Size in bytes of the FEC payload segments + /// * `num_data` - Number of data segments per FEC block + /// * `num_parity` - Number of parity segments per FEC block + /// * `fec_id` - FEC encoding identifier (usually 0) + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the sender could not be started + pub fn start_sender( + &self, + instance_id: SessionId, + buffer_space: u32, + segment_size: u16, + num_data: u16, + num_parity: u16, + fec_id: Option, + ) -> Result<&Self> { + let fec_id = fec_id.unwrap_or(0); + let success = unsafe { + NormStartSender( + self.handle, + instance_id, + buffer_space, + segment_size, + num_data, + num_parity, + fec_id, + ) + }; + bool_result(success, "Failed to start NORM sender")?; + Ok(self) + } + + /// Stop the session as a sender + pub fn stop_sender(&self) -> &Self { + unsafe { NormStopSender(self.handle) }; + self + } + + /// Start the session as a NORM receiver + /// + /// # Arguments + /// * `buffer_space` - Buffer size in bytes for the receiver + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the receiver could not be started + pub fn start_receiver(&self, buffer_space: u32) -> Result<&Self> { + let success = unsafe { NormStartReceiver(self.handle, buffer_space) }; + bool_result(success, "Failed to start NORM receiver")?; + Ok(self) + } + + /// Stop the session as a receiver + pub fn stop_receiver(&self) -> &Self { + unsafe { NormStopReceiver(self.handle) }; + self + } + + /// Set the transmission rate in bits per second + /// + /// # Arguments + /// * `bits_per_second` - The transmission rate in bits per second + /// + /// # Returns + /// A reference to self for method chaining + pub fn set_tx_rate(&self, bits_per_second: f64) -> &Self { + unsafe { NormSetTxRate(self.handle, bits_per_second) }; + self + } + + /// Get the current transmission rate in bits per second + /// + /// # Returns + /// The current transmission rate in bits per second + pub fn tx_rate(&self) -> f64 { + unsafe { NormGetTxRate(self.handle) } + } + + /// Set the socket buffer size for transmission + /// + /// # Arguments + /// * `buffer_size` - The socket buffer size in bytes + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the buffer size could not be set + pub fn set_tx_socket_buffer(&self, buffer_size: u32) -> Result<&Self> { + let success = unsafe { NormSetTxSocketBuffer(self.handle, buffer_size) }; + bool_result(success, "Failed to set TX socket buffer size")?; + Ok(self) + } + + /// Set the socket buffer size for reception + /// + /// # Arguments + /// * `buffer_size` - The socket buffer size in bytes + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the buffer size could not be set + pub fn set_rx_socket_buffer(&self, buffer_size: u32) -> Result<&Self> { + let success = unsafe { NormSetRxSocketBuffer(self.handle, buffer_size) }; + bool_result(success, "Failed to set RX socket buffer size")?; + Ok(self) + } + + /// Set the flow control factor + /// + /// # Arguments + /// * `flow_control_factor` - The flow control factor + /// + /// # Returns + /// A reference to self for method chaining + pub fn set_flow_control(&self, flow_control_factor: f64) -> &Self { + unsafe { NormSetFlowControl(self.handle, flow_control_factor) }; + self + } + + /// Enable or disable congestion control + /// + /// # Arguments + /// * `enable` - Whether to enable congestion control + /// * `adjust_rate` - Whether to adjust the rate based on congestion control + /// + /// # Returns + /// A reference to self for method chaining + pub fn set_congestion_control(&self, enable: bool, adjust_rate: bool) -> &Self { + unsafe { NormSetCongestionControl(self.handle, enable, adjust_rate) }; + self + } + + /// Set the transmission rate bounds + /// + /// # Arguments + /// * `rate_min` - The minimum transmission rate in bits per second + /// * `rate_max` - The maximum transmission rate in bits per second + /// + /// # Returns + /// A reference to self for method chaining + pub fn set_tx_rate_bounds(&self, rate_min: f64, rate_max: f64) -> &Self { + unsafe { NormSetTxRateBounds(self.handle, rate_min, rate_max) }; + self + } + + /// Set the multicast interface + /// + /// # Arguments + /// * `interface_name` - The name of the interface to use for multicast + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the interface could not be set + pub fn set_multicast_interface>(&self, interface_name: I) -> Result<&Self> { + let c_iface = string_to_c_string(interface_name.as_ref())?; + let success = unsafe { NormSetMulticastInterface(self.handle, c_iface.as_ptr()) }; + bool_result(success, "Failed to set multicast interface")?; + Ok(self) + } + + /// Set source-specific multicast (SSM) source address + /// + /// # Arguments + /// * `source_address` - The source address for SSM + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the SSM source could not be set + pub fn set_ssm>(&self, source_address: A) -> Result<&Self> { + let c_addr = string_to_c_string(source_address.as_ref())?; + let success = unsafe { NormSetSSM(self.handle, c_addr.as_ptr()) }; + bool_result(success, "Failed to set SSM source address")?; + Ok(self) + } + + /// Set the time-to-live (TTL) for multicast packets + /// + /// # Arguments + /// * `ttl` - The TTL value (0-255) + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the TTL could not be set + pub fn set_ttl(&self, ttl: u8) -> Result<&Self> { + let success = unsafe { NormSetTTL(self.handle, ttl) }; + bool_result(success, "Failed to set TTL")?; + Ok(self) + } + + /// Set the TOS (Type of Service) value for packets + /// + /// # Arguments + /// * `tos` - The TOS value (0-255) + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the TOS could not be set + pub fn set_tos(&self, tos: u8) -> Result<&Self> { + let success = unsafe { NormSetTOS(self.handle, tos) }; + bool_result(success, "Failed to set TOS")?; + Ok(self) + } + + /// Enable or disable loopback + /// + /// # Arguments + /// * `enable` - Whether to enable loopback + /// + /// # Returns + /// `Ok(())` on success or an `Err` if loopback could not be set + pub fn set_loopback(&self, enable: bool) -> Result<&Self> { + let success = unsafe { NormSetLoopback(self.handle, enable) }; + bool_result(success, "Failed to set loopback")?; + Ok(self) + } + + /// Enable or disable multicast loopback + /// + /// # Arguments + /// * `enable` - Whether to enable multicast loopback + /// + /// # Returns + /// `Ok(())` on success or an `Err` if multicast loopback could not be set + pub fn set_multicast_loopback(&self, enable: bool) -> Result<&Self> { + let success = unsafe { NormSetMulticastLoopback(self.handle, enable) }; + bool_result(success, "Failed to set multicast loopback")?; + Ok(self) + } + + /// Check if an address is a unicast address + /// + /// # Arguments + /// * `address` - The address to check + /// + /// # Returns + /// `true` if the address is a unicast address, `false` otherwise + pub fn is_unicast_address>(address: A) -> bool { + if let Ok(c_addr) = string_to_c_string(address.as_ref()) { + unsafe { NormIsUnicastAddress(c_addr.as_ptr()) } + } else { + false + } + } + + /// Set the GRTT (Group Round Trip Time) estimate + /// + /// # Arguments + /// * `grtt_estimate` - The GRTT estimate in seconds + /// + /// # Returns + /// A reference to self for method chaining + pub fn set_grtt_estimate(&self, grtt_estimate: f64) -> &Self { + unsafe { NormSetGrttEstimate(self.handle, grtt_estimate) }; + self + } + + /// Get the current GRTT estimate + /// + /// # Returns + /// The current GRTT estimate in seconds + pub fn grtt_estimate(&self) -> f64 { + unsafe { NormGetGrttEstimate(self.handle) } + } + + /// Enqueue a file for transmission + /// + /// # Arguments + /// * `file_path` - The path to the file to send + /// * `info` - Optional info data to associate with the file + /// + /// # Returns + /// A handle to the enqueued file object or an error if the file could not be enqueued + pub fn file_enqueue>(&self, file_path: P, info: Option<&[u8]>) -> Result { + let c_path = string_to_c_string(file_path.as_ref())?; + + let (info_ptr, info_len) = match info { + Some(i) => (i.as_ptr() as *const c_char, i.len()), + None => (ptr::null(), 0), + }; + + let handle = unsafe { + NormFileEnqueue(self.handle, c_path.as_ptr(), info_ptr, info_len as u32) + }; + + unsafe { + check_handle(handle, NORM_OBJECT_INVALID) + .map_err(|_| Error::FileError("Failed to enqueue file".to_string())) + .map(|h| Object::from_handle(h)) + } + } + + /// Enqueue data for transmission + /// + /// # Arguments + /// * `data` - The data to send + /// * `info` - Optional info data to associate with the data + /// + /// # Returns + /// A handle to the enqueued data object or an error if the data could not be enqueued + pub fn data_enqueue(&self, data: &[u8], info: Option<&[u8]>) -> Result { + let (info_ptr, info_len) = match info { + Some(i) => (i.as_ptr() as *const c_char, i.len()), + None => (ptr::null(), 0), + }; + + let handle = unsafe { + NormDataEnqueue( + self.handle, + data.as_ptr() as *const c_char, + data.len() as u32, + info_ptr, + info_len as u32, + ) + }; + + unsafe { + check_handle(handle, NORM_OBJECT_INVALID) + .map_err(|_| Error::OperationFailed("Failed to enqueue data".to_string())) + .map(|h| Object::from_handle(h)) + } + } + + /// Open a stream for transmission + /// + /// # Arguments + /// * `buffer_size` - The size of the stream buffer in bytes + /// * `info` - Optional info data to associate with the stream + /// + /// # Returns + /// A handle to the stream object or an error if the stream could not be opened + pub fn stream_open(&self, buffer_size: u32, info: Option<&[u8]>) -> Result { + let (info_ptr, info_len) = match info { + Some(i) => (i.as_ptr() as *const c_char, i.len()), + None => (ptr::null(), 0), + }; + + let handle = unsafe { + NormStreamOpen(self.handle, buffer_size, info_ptr, info_len as u32) + }; + + unsafe { + check_handle(handle, NORM_OBJECT_INVALID) + .map_err(|_| Error::OperationFailed("Failed to open stream".to_string())) + .map(|h| Object::from_handle(h)) + } + } + + /// Set a watermark for acknowledgment + /// + /// # Arguments + /// * `object` - The object to set as a watermark + /// * `override_flush` - Whether to override any active flush + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the watermark could not be set + pub fn set_watermark(&self, object: &Object, override_flush: bool) -> Result<()> { + let success = unsafe { + NormSetWatermark(self.handle, object.handle(), override_flush) + }; + bool_result(success, "Failed to set watermark") + } + + /// Reset the watermark + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the watermark could not be reset + pub fn reset_watermark(&self) -> Result<()> { + let success = unsafe { NormResetWatermark(self.handle) }; + bool_result(success, "Failed to reset watermark") + } + + /// Cancel the watermark + pub fn cancel_watermark(&self) { + unsafe { NormCancelWatermark(self.handle) }; + } + + /// Send a command to all receivers + /// + /// # Arguments + /// * `command` - The command data to send + /// * `robust` - Whether to send the command robustly (with more reliability) + /// + /// # Returns + /// `Ok(())` on success or an `Err` if the command could not be sent + pub fn send_command(&self, command: &[u8], robust: bool) -> Result<()> { + let success = unsafe { + NormSendCommand( + self.handle, + command.as_ptr() as *const c_char, + command.len() as u32, + robust, + ) + }; + bool_result(success, "Failed to send command") + } + + /// Cancel a command in progress + pub fn cancel_command(&self) { + unsafe { NormCancelCommand(self.handle) }; + } + + /// Get the raw NORM session handle + /// + /// # Returns + /// The raw NORM session handle + pub(crate) fn handle(&self) -> NormSessionHandle { + self.handle + } +} + +impl Drop for Session { + fn drop(&mut self) { + unsafe { NormDestroySession(self.handle) }; + } +} \ No newline at end of file diff --git a/src/rust/norm/src/types.rs b/src/rust/norm/src/types.rs new file mode 100644 index 0000000..b19f1ed --- /dev/null +++ b/src/rust/norm/src/types.rs @@ -0,0 +1,525 @@ +// Allow non-uppercase globals from FFI bindings +// The bindgen-generated constants use lowercase prefixes, which is acceptable for FFI code +#![allow(non_upper_case_globals)] + +use norm_sys::*; +use std::fmt; + +/// NORM node identifier type +pub type NodeId = u32; + +/// NORM session identifier type +pub type SessionId = u16; + +/// NORM object transport identifier type +pub type ObjectTransportId = u16; + +/// NORM size type for file and object sizes +#[cfg(unix)] +pub type Size = i64; + +/// NORM size type for file and object sizes +#[cfg(windows)] +pub type Size = i64; + +/// NORM object types +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ObjectType { + /// Placeholder for no object type + None = NormObjectType_NORM_OBJECT_NONE as u32, + /// Data object (memory buffer) + Data = NormObjectType_NORM_OBJECT_DATA as u32, + /// File object + File = NormObjectType_NORM_OBJECT_FILE as u32, + /// Stream object + Stream = NormObjectType_NORM_OBJECT_STREAM as u32, +} + +impl From for ObjectType { + fn from(t: NormObjectType) -> Self { + match t { + NormObjectType_NORM_OBJECT_NONE => ObjectType::None, + NormObjectType_NORM_OBJECT_DATA => ObjectType::Data, + NormObjectType_NORM_OBJECT_FILE => ObjectType::File, + NormObjectType_NORM_OBJECT_STREAM => ObjectType::Stream, + _ => ObjectType::None, + } + } +} + +impl From for NormObjectType { + fn from(t: ObjectType) -> Self { + match t { + ObjectType::None => NormObjectType_NORM_OBJECT_NONE, + ObjectType::Data => NormObjectType_NORM_OBJECT_DATA, + ObjectType::File => NormObjectType_NORM_OBJECT_FILE, + ObjectType::Stream => NormObjectType_NORM_OBJECT_STREAM, + } + } +} + +/// NORM flush modes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum FlushMode { + /// No flush + None = NormFlushMode_NORM_FLUSH_NONE as u32, + /// Passive flush - minimal delay + Passive = NormFlushMode_NORM_FLUSH_PASSIVE as u32, + /// Active flush - actively seek acknowledgment + Active = NormFlushMode_NORM_FLUSH_ACTIVE as u32, +} + +impl From for FlushMode { + fn from(m: NormFlushMode) -> Self { + match m { + NormFlushMode_NORM_FLUSH_NONE => FlushMode::None, + NormFlushMode_NORM_FLUSH_PASSIVE => FlushMode::Passive, + NormFlushMode_NORM_FLUSH_ACTIVE => FlushMode::Active, + _ => FlushMode::None, + } + } +} + +impl From for NormFlushMode { + fn from(m: FlushMode) -> Self { + match m { + FlushMode::None => NormFlushMode_NORM_FLUSH_NONE, + FlushMode::Passive => NormFlushMode_NORM_FLUSH_PASSIVE, + FlushMode::Active => NormFlushMode_NORM_FLUSH_ACTIVE, + } + } +} + +/// NORM nacking modes +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum NackingMode { + /// No NACKs + None = NormNackingMode_NORM_NACK_NONE as u32, + /// Info-only NACKs + InfoOnly = NormNackingMode_NORM_NACK_INFO_ONLY as u32, + /// Normal NACKs + Normal = NormNackingMode_NORM_NACK_NORMAL as u32, +} + +impl From for NackingMode { + fn from(m: NormNackingMode) -> Self { + match m { + NormNackingMode_NORM_NACK_NONE => NackingMode::None, + NormNackingMode_NORM_NACK_INFO_ONLY => NackingMode::InfoOnly, + NormNackingMode_NORM_NACK_NORMAL => NackingMode::Normal, + _ => NackingMode::None, + } + } +} + +impl From for NormNackingMode { + fn from(m: NackingMode) -> Self { + match m { + NackingMode::None => NormNackingMode_NORM_NACK_NONE, + NackingMode::InfoOnly => NormNackingMode_NORM_NACK_INFO_ONLY, + NackingMode::Normal => NormNackingMode_NORM_NACK_NORMAL, + } + } +} + +/// NORM acking status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum AckingStatus { + /// Invalid ack status + Invalid = NormAckingStatus_NORM_ACK_INVALID as u32, + /// Ack failure + Failure = NormAckingStatus_NORM_ACK_FAILURE as u32, + /// Ack pending + Pending = NormAckingStatus_NORM_ACK_PENDING as u32, + /// Ack success + Success = NormAckingStatus_NORM_ACK_SUCCESS as u32, +} + +impl From for AckingStatus { + fn from(s: NormAckingStatus) -> Self { + match s { + NormAckingStatus_NORM_ACK_INVALID => AckingStatus::Invalid, + NormAckingStatus_NORM_ACK_FAILURE => AckingStatus::Failure, + NormAckingStatus_NORM_ACK_PENDING => AckingStatus::Pending, + NormAckingStatus_NORM_ACK_SUCCESS => AckingStatus::Success, + _ => AckingStatus::Invalid, + } + } +} + +impl From for NormAckingStatus { + fn from(s: AckingStatus) -> Self { + match s { + AckingStatus::Invalid => NormAckingStatus_NORM_ACK_INVALID, + AckingStatus::Failure => NormAckingStatus_NORM_ACK_FAILURE, + AckingStatus::Pending => NormAckingStatus_NORM_ACK_PENDING, + AckingStatus::Success => NormAckingStatus_NORM_ACK_SUCCESS, + } + } +} + +/// NORM tracking status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum TrackingStatus { + /// No tracking + None = NormTrackingStatus_NORM_TRACK_NONE as u32, + /// Track receivers + Receivers = NormTrackingStatus_NORM_TRACK_RECEIVERS as u32, + /// Track senders + Senders = NormTrackingStatus_NORM_TRACK_SENDERS as u32, + /// Track all + All = NormTrackingStatus_NORM_TRACK_ALL as u32, +} + +impl From for TrackingStatus { + fn from(s: NormTrackingStatus) -> Self { + match s { + NormTrackingStatus_NORM_TRACK_NONE => TrackingStatus::None, + NormTrackingStatus_NORM_TRACK_RECEIVERS => TrackingStatus::Receivers, + NormTrackingStatus_NORM_TRACK_SENDERS => TrackingStatus::Senders, + NormTrackingStatus_NORM_TRACK_ALL => TrackingStatus::All, + _ => TrackingStatus::None, + } + } +} + +impl From for NormTrackingStatus { + fn from(s: TrackingStatus) -> Self { + match s { + TrackingStatus::None => NormTrackingStatus_NORM_TRACK_NONE, + TrackingStatus::Receivers => NormTrackingStatus_NORM_TRACK_RECEIVERS, + TrackingStatus::Senders => NormTrackingStatus_NORM_TRACK_SENDERS, + TrackingStatus::All => NormTrackingStatus_NORM_TRACK_ALL, + } + } +} + +/// NORM probing mode +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ProbingMode { + /// No probing + None = NormProbingMode_NORM_PROBE_NONE as u32, + /// Passive probing + Passive = NormProbingMode_NORM_PROBE_PASSIVE as u32, + /// Active probing + Active = NormProbingMode_NORM_PROBE_ACTIVE as u32, +} + +impl From for ProbingMode { + fn from(m: NormProbingMode) -> Self { + match m { + NormProbingMode_NORM_PROBE_NONE => ProbingMode::None, + NormProbingMode_NORM_PROBE_PASSIVE => ProbingMode::Passive, + NormProbingMode_NORM_PROBE_ACTIVE => ProbingMode::Active, + _ => ProbingMode::None, + } + } +} + +impl From for NormProbingMode { + fn from(m: ProbingMode) -> Self { + match m { + ProbingMode::None => NormProbingMode_NORM_PROBE_NONE, + ProbingMode::Passive => NormProbingMode_NORM_PROBE_PASSIVE, + ProbingMode::Active => NormProbingMode_NORM_PROBE_ACTIVE, + } + } +} + +/// NORM sync policy +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum SyncPolicy { + /// Sync to current data (join mid-stream) + Current = NormSyncPolicy_NORM_SYNC_CURRENT as u32, + /// Sync to stream from beginning + Stream = NormSyncPolicy_NORM_SYNC_STREAM as u32, + /// Sync to all data, old and new + All = NormSyncPolicy_NORM_SYNC_ALL as u32, +} + +impl From for SyncPolicy { + fn from(p: NormSyncPolicy) -> Self { + match p { + NormSyncPolicy_NORM_SYNC_CURRENT => SyncPolicy::Current, + NormSyncPolicy_NORM_SYNC_STREAM => SyncPolicy::Stream, + NormSyncPolicy_NORM_SYNC_ALL => SyncPolicy::All, + _ => SyncPolicy::Current, + } + } +} + +impl From for NormSyncPolicy { + fn from(p: SyncPolicy) -> Self { + match p { + SyncPolicy::Current => NormSyncPolicy_NORM_SYNC_CURRENT, + SyncPolicy::Stream => NormSyncPolicy_NORM_SYNC_STREAM, + SyncPolicy::All => NormSyncPolicy_NORM_SYNC_ALL, + } + } +} + +/// NORM repair boundary +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum RepairBoundary { + /// Block boundary + Block = NormRepairBoundary_NORM_BOUNDARY_BLOCK as u32, + /// Object boundary + Object = NormRepairBoundary_NORM_BOUNDARY_OBJECT as u32, +} + +impl From for RepairBoundary { + fn from(b: NormRepairBoundary) -> Self { + match b { + NormRepairBoundary_NORM_BOUNDARY_BLOCK => RepairBoundary::Block, + NormRepairBoundary_NORM_BOUNDARY_OBJECT => RepairBoundary::Object, + _ => RepairBoundary::Block, + } + } +} + +impl From for NormRepairBoundary { + fn from(b: RepairBoundary) -> Self { + match b { + RepairBoundary::Block => NormRepairBoundary_NORM_BOUNDARY_BLOCK, + RepairBoundary::Object => NormRepairBoundary_NORM_BOUNDARY_OBJECT, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_object_type_conversions() { + // Test From for ObjectType + let data_type: ObjectType = NormObjectType_NORM_OBJECT_DATA.into(); + assert_eq!(data_type, ObjectType::Data); + + let file_type: ObjectType = NormObjectType_NORM_OBJECT_FILE.into(); + assert_eq!(file_type, ObjectType::File); + + let stream_type: ObjectType = NormObjectType_NORM_OBJECT_STREAM.into(); + assert_eq!(stream_type, ObjectType::Stream); + + // Test From for NormObjectType + let norm_data: NormObjectType = ObjectType::Data.into(); + assert_eq!(norm_data, NormObjectType_NORM_OBJECT_DATA); + } + + #[test] + fn test_flush_mode_conversions() { + assert_eq!(FlushMode::from(NormFlushMode_NORM_FLUSH_NONE), FlushMode::None); + assert_eq!(FlushMode::from(NormFlushMode_NORM_FLUSH_PASSIVE), FlushMode::Passive); + assert_eq!(FlushMode::from(NormFlushMode_NORM_FLUSH_ACTIVE), FlushMode::Active); + + let norm_active: NormFlushMode = FlushMode::Active.into(); + assert_eq!(norm_active, NormFlushMode_NORM_FLUSH_ACTIVE); + } + + #[test] + fn test_event_type_conversions() { + let tx_event: EventType = NormEventType_NORM_TX_QUEUE_EMPTY.into(); + assert_eq!(tx_event, EventType::TxQueueEmpty); + + let rx_event: EventType = NormEventType_NORM_RX_OBJECT_COMPLETED.into(); + assert_eq!(rx_event, EventType::RxObjectCompleted); + + let norm_event: NormEventType = EventType::TxObjectSent.into(); + assert_eq!(norm_event, NormEventType_NORM_TX_OBJECT_SENT); + } + + #[test] + fn test_nacking_mode_conversions() { + assert_eq!(NackingMode::from(NormNackingMode_NORM_NACK_NONE), NackingMode::None); + assert_eq!(NackingMode::from(NormNackingMode_NORM_NACK_NORMAL), NackingMode::Normal); + } + + #[test] + fn test_acking_status_conversions() { + assert_eq!(AckingStatus::from(NormAckingStatus_NORM_ACK_SUCCESS), AckingStatus::Success); + assert_eq!(AckingStatus::from(NormAckingStatus_NORM_ACK_FAILURE), AckingStatus::Failure); + assert_eq!(AckingStatus::from(NormAckingStatus_NORM_ACK_PENDING), AckingStatus::Pending); + } +} + +/// NORM event type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum EventType { + /// Invalid event + Invalid = NormEventType_NORM_EVENT_INVALID as u32, + /// Transmit queue vacancy + TxQueueVacancy = NormEventType_NORM_TX_QUEUE_VACANCY as u32, + /// Transmit queue empty + TxQueueEmpty = NormEventType_NORM_TX_QUEUE_EMPTY as u32, + /// Transmit flush completed + TxFlushCompleted = NormEventType_NORM_TX_FLUSH_COMPLETED as u32, + /// Transmit watermark completed + TxWatermarkCompleted = NormEventType_NORM_TX_WATERMARK_COMPLETED as u32, + /// Transmit command sent + TxCmdSent = NormEventType_NORM_TX_CMD_SENT as u32, + /// Transmit object sent + TxObjectSent = NormEventType_NORM_TX_OBJECT_SENT as u32, + /// Transmit object purged + TxObjectPurged = NormEventType_NORM_TX_OBJECT_PURGED as u32, + /// Transmit rate changed + TxRateChanged = NormEventType_NORM_TX_RATE_CHANGED as u32, + /// Local sender closed + LocalSenderClosed = NormEventType_NORM_LOCAL_SENDER_CLOSED as u32, + /// Remote sender new + RemoteSenderNew = NormEventType_NORM_REMOTE_SENDER_NEW as u32, + /// Remote sender reset + RemoteSenderReset = NormEventType_NORM_REMOTE_SENDER_RESET as u32, + /// Remote sender address + RemoteSenderAddress = NormEventType_NORM_REMOTE_SENDER_ADDRESS as u32, + /// Remote sender active + RemoteSenderActive = NormEventType_NORM_REMOTE_SENDER_ACTIVE as u32, + /// Remote sender inactive + RemoteSenderInactive = NormEventType_NORM_REMOTE_SENDER_INACTIVE as u32, + /// Remote sender purged + RemoteSenderPurged = NormEventType_NORM_REMOTE_SENDER_PURGED as u32, + /// Receive command new + RxCmdNew = NormEventType_NORM_RX_CMD_NEW as u32, + /// Receive object new + RxObjectNew = NormEventType_NORM_RX_OBJECT_NEW as u32, + /// Receive object info + RxObjectInfo = NormEventType_NORM_RX_OBJECT_INFO as u32, + /// Receive object updated + RxObjectUpdated = NormEventType_NORM_RX_OBJECT_UPDATED as u32, + /// Receive object completed + RxObjectCompleted = NormEventType_NORM_RX_OBJECT_COMPLETED as u32, + /// Receive object aborted + RxObjectAborted = NormEventType_NORM_RX_OBJECT_ABORTED as u32, + /// Receive ack request + RxAckRequest = NormEventType_NORM_RX_ACK_REQUEST as u32, + /// GRTT updated + GrttUpdated = NormEventType_NORM_GRTT_UPDATED as u32, + /// CC active + CcActive = NormEventType_NORM_CC_ACTIVE as u32, + /// CC inactive + CcInactive = NormEventType_NORM_CC_INACTIVE as u32, + /// Acking node new + AckingNodeNew = NormEventType_NORM_ACKING_NODE_NEW as u32, + /// Send error + SendError = NormEventType_NORM_SEND_ERROR as u32, + /// User timeout + UserTimeout = NormEventType_NORM_USER_TIMEOUT as u32, +} + +impl From for EventType { + fn from(t: NormEventType) -> Self { + match t { + NormEventType_NORM_EVENT_INVALID => EventType::Invalid, + NormEventType_NORM_TX_QUEUE_VACANCY => EventType::TxQueueVacancy, + NormEventType_NORM_TX_QUEUE_EMPTY => EventType::TxQueueEmpty, + NormEventType_NORM_TX_FLUSH_COMPLETED => EventType::TxFlushCompleted, + NormEventType_NORM_TX_WATERMARK_COMPLETED => EventType::TxWatermarkCompleted, + NormEventType_NORM_TX_CMD_SENT => EventType::TxCmdSent, + NormEventType_NORM_TX_OBJECT_SENT => EventType::TxObjectSent, + NormEventType_NORM_TX_OBJECT_PURGED => EventType::TxObjectPurged, + NormEventType_NORM_TX_RATE_CHANGED => EventType::TxRateChanged, + NormEventType_NORM_LOCAL_SENDER_CLOSED => EventType::LocalSenderClosed, + NormEventType_NORM_REMOTE_SENDER_NEW => EventType::RemoteSenderNew, + NormEventType_NORM_REMOTE_SENDER_RESET => EventType::RemoteSenderReset, + NormEventType_NORM_REMOTE_SENDER_ADDRESS => EventType::RemoteSenderAddress, + NormEventType_NORM_REMOTE_SENDER_ACTIVE => EventType::RemoteSenderActive, + NormEventType_NORM_REMOTE_SENDER_INACTIVE => EventType::RemoteSenderInactive, + NormEventType_NORM_REMOTE_SENDER_PURGED => EventType::RemoteSenderPurged, + NormEventType_NORM_RX_CMD_NEW => EventType::RxCmdNew, + NormEventType_NORM_RX_OBJECT_NEW => EventType::RxObjectNew, + NormEventType_NORM_RX_OBJECT_INFO => EventType::RxObjectInfo, + NormEventType_NORM_RX_OBJECT_UPDATED => EventType::RxObjectUpdated, + NormEventType_NORM_RX_OBJECT_COMPLETED => EventType::RxObjectCompleted, + NormEventType_NORM_RX_OBJECT_ABORTED => EventType::RxObjectAborted, + NormEventType_NORM_RX_ACK_REQUEST => EventType::RxAckRequest, + NormEventType_NORM_GRTT_UPDATED => EventType::GrttUpdated, + NormEventType_NORM_CC_ACTIVE => EventType::CcActive, + NormEventType_NORM_CC_INACTIVE => EventType::CcInactive, + NormEventType_NORM_ACKING_NODE_NEW => EventType::AckingNodeNew, + NormEventType_NORM_SEND_ERROR => EventType::SendError, + NormEventType_NORM_USER_TIMEOUT => EventType::UserTimeout, + _ => EventType::Invalid, + } + } +} + +impl From for NormEventType { + fn from(t: EventType) -> Self { + match t { + EventType::Invalid => NormEventType_NORM_EVENT_INVALID, + EventType::TxQueueVacancy => NormEventType_NORM_TX_QUEUE_VACANCY, + EventType::TxQueueEmpty => NormEventType_NORM_TX_QUEUE_EMPTY, + EventType::TxFlushCompleted => NormEventType_NORM_TX_FLUSH_COMPLETED, + EventType::TxWatermarkCompleted => NormEventType_NORM_TX_WATERMARK_COMPLETED, + EventType::TxCmdSent => NormEventType_NORM_TX_CMD_SENT, + EventType::TxObjectSent => NormEventType_NORM_TX_OBJECT_SENT, + EventType::TxObjectPurged => NormEventType_NORM_TX_OBJECT_PURGED, + EventType::TxRateChanged => NormEventType_NORM_TX_RATE_CHANGED, + EventType::LocalSenderClosed => NormEventType_NORM_LOCAL_SENDER_CLOSED, + EventType::RemoteSenderNew => NormEventType_NORM_REMOTE_SENDER_NEW, + EventType::RemoteSenderReset => NormEventType_NORM_REMOTE_SENDER_RESET, + EventType::RemoteSenderAddress => NormEventType_NORM_REMOTE_SENDER_ADDRESS, + EventType::RemoteSenderActive => NormEventType_NORM_REMOTE_SENDER_ACTIVE, + EventType::RemoteSenderInactive => NormEventType_NORM_REMOTE_SENDER_INACTIVE, + EventType::RemoteSenderPurged => NormEventType_NORM_REMOTE_SENDER_PURGED, + EventType::RxCmdNew => NormEventType_NORM_RX_CMD_NEW, + EventType::RxObjectNew => NormEventType_NORM_RX_OBJECT_NEW, + EventType::RxObjectInfo => NormEventType_NORM_RX_OBJECT_INFO, + EventType::RxObjectUpdated => NormEventType_NORM_RX_OBJECT_UPDATED, + EventType::RxObjectCompleted => NormEventType_NORM_RX_OBJECT_COMPLETED, + EventType::RxObjectAborted => NormEventType_NORM_RX_OBJECT_ABORTED, + EventType::RxAckRequest => NormEventType_NORM_RX_ACK_REQUEST, + EventType::GrttUpdated => NormEventType_NORM_GRTT_UPDATED, + EventType::CcActive => NormEventType_NORM_CC_ACTIVE, + EventType::CcInactive => NormEventType_NORM_CC_INACTIVE, + EventType::AckingNodeNew => NormEventType_NORM_ACKING_NODE_NEW, + EventType::SendError => NormEventType_NORM_SEND_ERROR, + EventType::UserTimeout => NormEventType_NORM_USER_TIMEOUT, + } + } +} + +impl fmt::Display for EventType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EventType::Invalid => write!(f, "Invalid"), + EventType::TxQueueVacancy => write!(f, "TxQueueVacancy"), + EventType::TxQueueEmpty => write!(f, "TxQueueEmpty"), + EventType::TxFlushCompleted => write!(f, "TxFlushCompleted"), + EventType::TxWatermarkCompleted => write!(f, "TxWatermarkCompleted"), + EventType::TxCmdSent => write!(f, "TxCmdSent"), + EventType::TxObjectSent => write!(f, "TxObjectSent"), + EventType::TxObjectPurged => write!(f, "TxObjectPurged"), + EventType::TxRateChanged => write!(f, "TxRateChanged"), + EventType::LocalSenderClosed => write!(f, "LocalSenderClosed"), + EventType::RemoteSenderNew => write!(f, "RemoteSenderNew"), + EventType::RemoteSenderReset => write!(f, "RemoteSenderReset"), + EventType::RemoteSenderAddress => write!(f, "RemoteSenderAddress"), + EventType::RemoteSenderActive => write!(f, "RemoteSenderActive"), + EventType::RemoteSenderInactive => write!(f, "RemoteSenderInactive"), + EventType::RemoteSenderPurged => write!(f, "RemoteSenderPurged"), + EventType::RxCmdNew => write!(f, "RxCmdNew"), + EventType::RxObjectNew => write!(f, "RxObjectNew"), + EventType::RxObjectInfo => write!(f, "RxObjectInfo"), + EventType::RxObjectUpdated => write!(f, "RxObjectUpdated"), + EventType::RxObjectCompleted => write!(f, "RxObjectCompleted"), + EventType::RxObjectAborted => write!(f, "RxObjectAborted"), + EventType::RxAckRequest => write!(f, "RxAckRequest"), + EventType::GrttUpdated => write!(f, "GrttUpdated"), + EventType::CcActive => write!(f, "CcActive"), + EventType::CcInactive => write!(f, "CcInactive"), + EventType::AckingNodeNew => write!(f, "AckingNodeNew"), + EventType::SendError => write!(f, "SendError"), + EventType::UserTimeout => write!(f, "UserTimeout"), + } + } +} \ No newline at end of file diff --git a/src/rust/target/.rustc_info.json b/src/rust/target/.rustc_info.json new file mode 100644 index 0000000..9dab362 --- /dev/null +++ b/src/rust/target/.rustc_info.json @@ -0,0 +1 @@ +{"rustc_fingerprint":18041410539700412860,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.90.0 (1159e78c4 2025-09-14)\nbinary: rustc\ncommit-hash: 1159e78c4747b02ef996e55082b704c09b970588\ncommit-date: 2025-09-14\nhost: aarch64-apple-darwin\nrelease: 1.90.0\nLLVM version: 20.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/cbowers/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/src/rust/target/CACHEDIR.TAG b/src/rust/target/CACHEDIR.TAG new file mode 100644 index 0000000..20d7c31 --- /dev/null +++ b/src/rust/target/CACHEDIR.TAG @@ -0,0 +1,3 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by cargo. +# For information about cache directory tags see https://bford.info/cachedir/ diff --git a/src/rust/waf_rust.py b/src/rust/waf_rust.py new file mode 100644 index 0000000..b9a69b9 --- /dev/null +++ b/src/rust/waf_rust.py @@ -0,0 +1,131 @@ +# Helper module for integrating Rust bindings with NORM's waf build system + +import os +import subprocess +from waflib import TaskGen, Task, Logs, Errors, Context + +def options(ctx): + """Add options for Rust integration to waf""" + ctx.add_option('--build-rust', action='store_true', default=False, + help='Build Rust bindings for NORM') + ctx.add_option('--rust-release', action='store_true', default=False, + help='Build Rust bindings in release mode') + ctx.add_option('--rust-docs', action='store_true', default=False, + help='Generate documentation for Rust bindings') + +def configure(ctx): + """Configure Rust toolchain if requested""" + if ctx.options.build_rust: + # Check for Rust toolchain + try: + ctx.find_program('cargo', var='CARGO', mandatory=True) + rustc_cmd = ctx.find_program('rustc', var='RUSTC', mandatory=True) + + # Get Rust version + rustc_ver = subprocess.check_output([rustc_cmd[0], '--version']).decode('utf-8') + ctx.msg('Checking for rustc version', rustc_ver.strip()) + + # Set rust environment variables + ctx.env.BUILD_RUST = True + ctx.env.RUST_RELEASE = ctx.options.rust_release + ctx.env.RUST_DOCS = ctx.options.rust_docs + + except Errors.WafError: + ctx.fatal('Rust toolchain not found. Install Rust from https://rustup.rs') + +def build_rust_bindings(ctx): + """Build the Rust bindings using cargo""" + if not ctx.env.BUILD_RUST: + return + + rust_dir = ctx.path.find_dir('src/rust') + if not rust_dir: + ctx.fatal('Rust directory not found. Expected at: ' + ctx.path.abspath() + '/src/rust') + + # Set environment variables for cargo + cargo_env = os.environ.copy() + cargo_env['NORM_INCLUDE_DIR'] = os.path.abspath('include') + cargo_env['NORM_LIB_DIR'] = os.path.abspath('lib') + + # Prepare cargo command + cargo_cmd = [ctx.env.CARGO[0], 'build'] + if ctx.env.RUST_RELEASE: + cargo_cmd.append('--release') + + # Build Rust bindings + cwd = rust_dir.abspath() + try: + Logs.info("Building Rust bindings in: " + cwd) + ret = subprocess.call(cargo_cmd, env=cargo_env, cwd=cwd) + if ret != 0: + ctx.fatal('Failed to build Rust bindings') + + # Build documentation if requested + if ctx.env.RUST_DOCS: + Logs.info("Building Rust documentation") + doc_cmd = [ctx.env.CARGO[0], 'doc', '--no-deps'] + if ctx.env.RUST_RELEASE: + doc_cmd.append('--release') + + ret = subprocess.call(doc_cmd, env=cargo_env, cwd=cwd) + if ret != 0: + Logs.warn('Failed to build Rust documentation') + + except Exception as e: + ctx.fatal('Error building Rust bindings: ' + str(e)) + +def install_rust_bindings(ctx): + """Install the Rust bindings if requested""" + if not ctx.env.BUILD_RUST: + return + + # Define installation directories + rust_dir = ctx.path.find_dir('src/rust') + build_type = 'release' if ctx.env.RUST_RELEASE else 'debug' + target_dir = rust_dir.find_dir('target') + + # Install libraries + if target_dir: + lib_dir = target_dir.find_dir(build_type) + if lib_dir: + # Install .rlib files + ctx.install_files( + '${LIBDIR}/rust', + lib_dir.ant_glob('*.rlib'), + postpone=False + ) + + # Install dynamic libraries + if ctx.env.DEST_OS == 'win32': + ctx.install_files( + '${BINDIR}', + lib_dir.ant_glob('*.dll'), + postpone=False + ) + else: + ctx.install_files( + '${LIBDIR}', + lib_dir.ant_glob('*.so') + lib_dir.ant_glob('*.dylib'), + postpone=False + ) + + # Install documentation + if ctx.env.RUST_DOCS and target_dir: + doc_dir = target_dir.find_dir('doc') + if doc_dir: + ctx.install_files( + '${PREFIX}/share/doc/norm-rust', + doc_dir.ant_glob('**/*'), + cwd=doc_dir, + relative_trick=True, + postpone=False + ) + + # Install Cargo.toml and source files for developers + ctx.install_files( + '${PREFIX}/share/norm/rust', + rust_dir.ant_glob('**/*.rs') + rust_dir.ant_glob('**/*.toml'), + cwd=rust_dir, + relative_trick=True, + postpone=False + ) \ No newline at end of file diff --git a/wscript b/wscript index eda5d13..46965d1 100755 --- a/wscript +++ b/wscript @@ -19,6 +19,8 @@ To build examples, use the --target directive. For example: import platform import waflib +import sys +import os # Fetch VERSION from include/normVersion.h file VERSION = None @@ -47,9 +49,27 @@ def options(ctx): ctx.recurse('protolib') build_opts = ctx.parser.add_argument_group('Compile/install Options', 'Use during build/install step.') + # Add Rust binding options + ctx.add_option('--build-rust', action='store_true', default=False, + help='Build Rust bindings for NORM') + ctx.add_option('--rust-release', action='store_true', default=False, + help='Build Rust bindings in release mode') + ctx.add_option('--rust-docs', action='store_true', default=False, + help='Generate documentation for Rust bindings') + def configure(ctx): ctx.recurse('protolib') + # Configure Rust bindings if requested + if ctx.options.build_rust: + try: + ctx.find_program('cargo', var='CARGO', mandatory=True) + ctx.env.BUILD_RUST = True + ctx.env.RUST_RELEASE = ctx.options.rust_release + ctx.env.RUST_DOCS = ctx.options.rust_docs + except ctx.errors.ConfigurationError: + ctx.fatal('Rust toolchain not found. Install Rust from https://rustup.rs') + # Use this USE variable to add flags to NORM's compilation ctx.env.USE_BUILD_NORM += ['BUILD_NORM'] @@ -70,6 +90,16 @@ def configure(ctx): def build(ctx): ctx.recurse('protolib') + + # Build Rust bindings if configured + if hasattr(ctx.env, 'BUILD_RUST') and ctx.env.BUILD_RUST: + # Import our custom Rust build module + sys.path.insert(0, os.path.join(ctx.path.abspath(), 'src/rust')) + try: + import waf_rust + waf_rust.build_rust_bindings(ctx) + except ImportError: + ctx.fatal('Failed to import Rust waf module. Check src/rust/waf_rust.py exists.') # Setup to install NORM header file ctx.install_files("${PREFIX}/include/", "include/normApi.h") @@ -266,4 +296,4 @@ def _make_simple_example(ctx, name, path='examples'): if 'windows' == system: example.defines.append('_CONSOLE') example.stlib = (["Shell32"]) - + \ No newline at end of file