diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..667700a2 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,143 @@ +name: Test the rust bindings + +on: + pull_request: + workflow_dispatch: + workflow_call: # We would like this to be called by the main job + +jobs: + dependencies: + name: Check Rust dependencies + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./rust + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-deps-${{ hashFiles('**/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo-deps- + - name: Use Rust stable 1.88 + uses: dtolnay/rust-toolchain@1.88.0 + - name: Install cargo audit + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-audit + - name: Install cargo outdated + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-outdated + - name: Install cargo udeps + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-udeps + - name: Install cargo deny + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-deny + - name: Audit dependencies + run: cargo audit -D warnings + - name: Outdated dependencies + run: cargo outdated -d 1 -w --exit-code 1 + - name: Install nightly-2025-06-26 # Same date as 1.88 release + uses: dtolnay/rust-toolchain@nightly + with: + toolchain: nightly-2025-06-26 + - name: Unused depedency check + run: cargo udeps --all-targets + + lint: + name: Perform Rust linting and documentation + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./rust + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-lint-${{ hashFiles('**/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo-lint- + - name: Use Rust stable 1.88 + uses: dtolnay/rust-toolchain@1.88.0 + with: + components: rustfmt, clippy + - name: build + run: cargo build --locked + - name: Format + run: cargo fmt -- --check + - name: Docs + run: cargo doc --all-features + env: + RUSTDOCFLAGS: "-D warnings" + - name: Clippy + run: cargo clippy --all-targets -F mxl-not-built -- -D warnings -W clippy::pedantic + + tests: + name: Run the tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./rust + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-tests-${{ hashFiles('**/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo-tests- + - name: Use Rust stable 1.88 + uses: dtolnay/rust-toolchain@1.88.0 + with: + components: rustfmt, clippy + - name: build + run: cargo build --locked -F mxl-not-built + - name: Test + run: cargo test --locked -F mxl-not-built + - name: Coverage + run: > + cargo llvm-cov --ignore-filename-regex "build.rs|ffi.rs|(.*)_test.rs" + --lcov --output-path lcov.info + - name: Report Coverage + uses: romeovs/lcov-reporter-action@v0.4.0 + if: ${{ github.event_name == 'pull_request' }} + with: + lcov-file: ./lcov.info + github-token: ${{ secrets.GITHUB_TOKEN }} + delete-old-comments: true diff --git a/.vscode/settings.json b/.vscode/settings.json index ae574356..93f997e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project https://github.com/dmf-mxl/mxl/contributors.md // SPDX-License-Identifier: Apache-2.0 { + "rust-analyzer.linkedProjects": [ + "rust/Cargo.toml" + ], "files.associations": { "array": "cpp", "atomic": "cpp", @@ -71,4 +74,4 @@ "typeinfo": "cpp", "variant": "cpp" } -} \ No newline at end of file +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e62fe19..72b11e1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,9 @@ else() string(APPEND mxl_VERSION ".0") endif() +option(BUILD_TESTS "Build the tests" ON) +option(BUILD_TOOLS "Build the tools" ON) + project(mxl VERSION ${mxl_VERSION} LANGUAGES CXX C @@ -54,8 +57,10 @@ if(APPLE) endif() add_subdirectory(lib) -add_subdirectory(tools) add_subdirectory(utils) +if (BUILD_TOOLS) + add_subdirectory(tools) +endif() find_package(Doxygen) @@ -132,4 +137,4 @@ set(CPACK_PACKAGE_CONTACT "DMF MXL ") set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") # Include CPack -include(CPack) \ No newline at end of file +include(CPack) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 14bcdb74..d716dcbd 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -89,7 +89,9 @@ target_link_libraries(mxl # Alias trace to libtrace::libtrace so that this library can be used # in lieu of a module from the local source tree add_library(${PROJECT_NAME}::mxl ALIAS mxl) -add_subdirectory(tests) +if (BUILD_TESTS) + add_subdirectory(tests) +endif() # Install targets install(TARGETS mxl EXPORT ${PROJECT_NAME}-targets @@ -162,4 +164,4 @@ install(EXPORT ${PROJECT_NAME}-targets NAMESPACE ${PROJECT_NAME}:: DESTINATION ${MXL_CMAKE_CONFIG_DESTINATION} COMPONENT ${PROJECT_NAME}-dev - ) \ No newline at end of file + ) diff --git a/rust/.gitattributes b/rust/.gitattributes new file mode 100644 index 00000000..383338e0 --- /dev/null +++ b/rust/.gitattributes @@ -0,0 +1 @@ +Cargo.lock -diff diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 00000000..09c22a05 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.idea +.vscode +target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..eda5e2d4 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,690 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "annotate-snippets", + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[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 = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mxl" +version = "0.1.0" +dependencies = [ + "clap", + "dlopen2", + "mxl-sys", + "thiserror", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "mxl-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "cmake", +] + +[[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 = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..65ca6f08 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +members = ["mxl", "mxl-sys"] + +resolver = "2" + +[workspace.package] +edition = "2024" +publish = false +version = "0.1.0" +license = "Apache-2.0" +license-file = "../LICENSE.txt" + +[workspace.dependencies] +bindgen = { version = "0.72", features = ["experimental"] } +dlopen2 = "0.8" +# Will be used later, when we get to higher level streams based interfaces. +futures = "0.3" +thiserror = "2.0.12" +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter", "std"] } +uuid = { version = "1.17" } + +[workspace.dependencies.clap] +version = "4.1.4" +default-features = false +features = ["std", "derive", "cargo", "env", "help", "usage", "error-context"] + +[profile.release-with-debug] +debug = true +inherits = "release" diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 00000000..8dcb8a22 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,25 @@ +# Rust bindings for DMF MXL + +## Goals + +- Hide all the unsafe stuff inside these bindings. +- Provide more Rust-native like experience (async API based on `futures::stream` and + `futures::sink`?). + +## Code Guidelines + +- Use `rustfmt` in it's default settings for code formatting. +- The `cargo clippy` should be always clean. +- Try to avoid adding more dependencies, unless really necessary. +- Never use `unwrap`, `expect`, or a similar construct that causes a panic. Always return errors. Tests are an exception. + +## Building + +- `cargo build` + +## TODO + +- Get rid of the headers copy. Use the main headers as part of the build process. +- Change the tests so they can use libraries build from the main repo. +- Setup CI/CD. +- Extend the functionality. diff --git a/rust/flake.lock b/rust/flake.lock new file mode 100644 index 00000000..e69de29b diff --git a/rust/flake.nix b/rust/flake.nix new file mode 100644 index 00000000..a219b532 --- /dev/null +++ b/rust/flake.nix @@ -0,0 +1,70 @@ +{ + description = "Flake for MXL dev"; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/*.tar.gz"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + rust-overlay + }: let + overlays = [ + (import rust-overlay) + (self: super: { + rustStable = super.rust-bin.stable."1.88.0".default; + rustNightly = super.rust-bin.nightly."2025-06-26".default; + }) + ]; + + allSystems = [ + "x86_64-linux" # 64-bit Intel/AMD Linux + "aarch64-linux" # 64-bit ARM Linux + "x86_64-darwin" # 64-bit Intel macOS + "aarch64-darwin" # 64-bit ARM macOS + ]; + + forAllSystems = f: + nixpkgs.lib.genAttrs allSystems (system: + f { + pkgs = import nixpkgs { + inherit overlays system; + }; + } + ); + in { + devShells = forAllSystems ({pkgs}: { + default = pkgs.mkShell { + LIBCLANG_PATH = pkgs.lib.makeLibraryPath [pkgs.llvmPackages_latest.libclang.lib]; + packages = + (with pkgs; [ + rustStable + rust-analyzer + clang + cmake + pkg-config + ]); + }; + } + ); + nightly = forAllSystems ({pkgs}: { + default = pkgs.mkShell { + LIBCLANG_PATH = pkgs.lib.makeLibraryPath [pkgs.llvmPackages_latest.libclang.lib]; + packages = + (with pkgs; [ + rustNightly + rust-analyzer + clang + cmake + pkg-config + ]); + }; + } + ); + }; +} diff --git a/rust/mxl-sys/.gitignore b/rust/mxl-sys/.gitignore new file mode 100644 index 00000000..8d6e3400 --- /dev/null +++ b/rust/mxl-sys/.gitignore @@ -0,0 +1 @@ +mxl/version.h diff --git a/rust/mxl-sys/Cargo.toml b/rust/mxl-sys/Cargo.toml new file mode 100644 index 00000000..0e4fa65a --- /dev/null +++ b/rust/mxl-sys/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mxl-sys" +edition.workspace = true +publish.workspace = true +version.workspace = true + +[dependencies] + +[build-dependencies] +bindgen.workspace = true +cmake = "0.1.54" + +[features] +mxl-not-built = [] diff --git a/rust/mxl-sys/build.rs b/rust/mxl-sys/build.rs new file mode 100644 index 00000000..390ce970 --- /dev/null +++ b/rust/mxl-sys/build.rs @@ -0,0 +1,100 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +#[cfg(debug_assertions)] +const BUILD_VARIANT: &str = "Linux-Clang-Debug"; +#[cfg(not(debug_assertions))] +const BUILD_VARIANT: &str = "Linux-Clang-Release"; + +struct BindgenSpecs { + header: String, + includes_dirs: Vec, +} + +fn get_bindgen_specs() -> BindgenSpecs { + #[cfg(not(feature = "mxl-not-built"))] + let header = "wrapper-with-version-h.h".to_string(); + #[cfg(feature = "mxl-not-built")] + let header = "wrapper-without-version-h.h".to_string(); + + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("failed to get current directory")); + let repo_root = manifest_dir.parent().unwrap().parent().unwrap(); + let mut includes_dirs = vec![ + repo_root + .join("lib") + .join("include") + .to_string_lossy() + .to_string(), + ]; + if cfg!(not(feature = "mxl-not-built")) { + let build_dir = repo_root.join("build").join(BUILD_VARIANT); + let build_version_dir = build_dir + .join("lib") + .join("include") + .to_string_lossy() + .to_string(); + + includes_dirs.push(build_version_dir); + + let mxl_version_out_path = manifest_dir.join("mxl"); + if !fs::exists(&mxl_version_out_path) + .expect("Error checking if out path for version header file exists") + { + fs::create_dir(&mxl_version_out_path) + .expect("Failed to create out path for version header file"); + } + let mxl_version_header = mxl_version_out_path.join("version.h"); + println!("cargo:rerun-if-changed={}", mxl_version_header.display()); + // TODO: re-run on build_dir changing? + + let dst = cmake::Config::new(repo_root) + .out_dir(build_dir) + .generator("Unix Makefiles") + .define("BUILD_TESTS", "OFF") + .define("BUILD_TOOLS", "OFF") + // .configure_arg(format!("--preset={BUILD_VARIANT}")) + .build(); + + let mxl_version_location = dst.join("include").join("mxl").join("version.h"); + assert!(matches!(std::fs::exists(&mxl_version_location), Ok(true))); + + fs::copy(&mxl_version_location, &mxl_version_header) + .expect("Could copy mxl version header"); + + println!("cargo:rustc-link-search={}", dst.join("lib64").display()); + println!("cargo:rustc-link-lib=mxl"); + } + + BindgenSpecs { + header, + includes_dirs, + } +} + +fn main() { + let bindgen_specs = get_bindgen_specs(); + for include_dir in &bindgen_specs.includes_dirs { + println!("cargo:include={include_dir}"); + } + + let bindings = bindgen::builder() + .clang_args( + bindgen_specs + .includes_dirs + .iter() + .map(|dir| format!("-I{dir}")), + ) + .header(bindgen_specs.header) + .derive_default(true) + .derive_debug(true) + .prepend_enum_name(false) + .generate() + .unwrap(); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Could not write bindings"); +} diff --git a/rust/mxl-sys/src/lib.rs b/rust/mxl-sys/src/lib.rs new file mode 100644 index 00000000..666331b0 --- /dev/null +++ b/rust/mxl-sys/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(missing_docs)] +// Suppress expected warnings from bindgen-generated code. +// See https://github.com/rust-lang/rust-bindgen/issues/1651. +#![allow(deref_nullptr)] +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/rust/mxl-sys/tests/simple_test.rs b/rust/mxl-sys/tests/simple_test.rs new file mode 100644 index 00000000..c617fe56 --- /dev/null +++ b/rust/mxl-sys/tests/simple_test.rs @@ -0,0 +1,11 @@ +#[test] +fn there_is_bindgen_generated_code() { + let mxl_version = mxl_sys::mxlVersionType { + major: 3, + minor: 2, + bugfix: 1, + ..Default::default() + }; + + println!("mxl_version: {:?}", mxl_version); +} diff --git a/rust/mxl-sys/wrapper-with-version-h.h b/rust/mxl-sys/wrapper-with-version-h.h new file mode 100644 index 00000000..80c5254f --- /dev/null +++ b/rust/mxl-sys/wrapper-with-version-h.h @@ -0,0 +1,2 @@ +#include "wrapper-without-version-h.h" +#include "mxl/version.h" diff --git a/rust/mxl-sys/wrapper-without-version-h.h b/rust/mxl-sys/wrapper-without-version-h.h new file mode 100644 index 00000000..5248f1e1 --- /dev/null +++ b/rust/mxl-sys/wrapper-without-version-h.h @@ -0,0 +1,7 @@ +#include "mxl/dataformat.h" +#include "mxl/flow.h" +#include "mxl/flowinfo.h" +#include "mxl/mxl.h" +#include "mxl/platform.h" +#include "mxl/rational.h" +#include "mxl/time.h" diff --git a/rust/mxl/Cargo.toml b/rust/mxl/Cargo.toml new file mode 100644 index 00000000..447c4998 --- /dev/null +++ b/rust/mxl/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mxl" +edition.workspace = true +publish.workspace = true +version.workspace = true + +[dependencies] +mxl-sys = { path = "../mxl-sys" } + +dlopen2.workspace = true +thiserror.workspace = true +tracing.workspace = true +uuid.workspace = true + +[dev-dependencies] +clap.workspace = true +tracing-subscriber.workspace = true + +[features] +mxl-not-built = ["mxl-sys/mxl-not-built"] diff --git a/rust/mxl/build.rs b/rust/mxl/build.rs new file mode 100644 index 00000000..382d75e1 --- /dev/null +++ b/rust/mxl/build.rs @@ -0,0 +1,25 @@ +use std::env; +use std::path::PathBuf; + +#[cfg(debug_assertions)] +const BUILD_VARIANT: &str = "Linux-Clang-Debug"; +#[cfg(not(debug_assertions))] +const BUILD_VARIANT: &str = "Linux-Clang-Release"; + +fn main() { + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("failed to get current directory")); + let repo_root = manifest_dir.parent().unwrap().parent().unwrap(); + let build_dir = repo_root.join("build").join(BUILD_VARIANT); + + let out_path = PathBuf::from(env::var("OUT_DIR").expect("failed to get output directory")) + .join("constants.rs"); + + let data = format!( + "pub const MXL_REPO_ROOT: &str = \"{}\";\n\ + pub const MXL_BUILD_DIR: &str = \"{}\";\n", + repo_root.to_string_lossy(), + build_dir.to_string_lossy() + ); + std::fs::write(out_path, data).expect("Unable to write file"); +} diff --git a/rust/mxl/examples/common/mod.rs b/rust/mxl/examples/common/mod.rs new file mode 100644 index 00000000..221a4629 --- /dev/null +++ b/rust/mxl/examples/common/mod.rs @@ -0,0 +1,9 @@ +pub fn setup_logging() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); +} diff --git a/rust/mxl/examples/flow-reader.rs b/rust/mxl/examples/flow-reader.rs new file mode 100644 index 00000000..98bd2a2c --- /dev/null +++ b/rust/mxl/examples/flow-reader.rs @@ -0,0 +1,156 @@ +mod common; + +use std::time::Duration; + +use clap::Parser; +use mxl::config::get_mxf_so_path; +use tracing::{info, warn}; + +const READ_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug, Parser)] +#[command(version = clap::crate_version!(), author = clap::crate_authors!())] +pub struct Opts { + /// The path to the shmem directory where the mxl domain is mapped. + #[arg(long)] + pub mxl_domain: String, + + /// The id of the flow to read. + #[arg(long)] + pub flow_id: String, + + /// The number of samples to be read in one open samples call. Is only valid for "continuous" + /// flows. If not specified, the value provided by the writer will be used if available, or this + /// will more or less fit 10 ms as a fallback. + #[arg(long)] + pub sample_batch_size: Option, +} + +fn main() -> Result<(), mxl::Error> { + common::setup_logging(); + let opts: Opts = Opts::parse(); + + let mxl_api = mxl::load_api(get_mxf_so_path())?; + let mxl_instance = mxl::MxlInstance::new(mxl_api, &opts.mxl_domain, "")?; + let reader = mxl_instance.create_flow_reader(&opts.flow_id)?; + let flow_info = reader.get_info()?; + if flow_info.is_discrete_flow() { + if opts.sample_batch_size.is_some() { + return Err(mxl::Error::Other( + "Sample batch size is only relevant for \"continuous\" flows.".to_owned(), + )); + } + read_grains(mxl_instance, reader.to_grain_reader()?, flow_info) + } else { + read_samples( + mxl_instance, + reader.to_samples_reader()?, + flow_info, + opts.sample_batch_size, + ) + } +} + +fn read_grains( + mxl_instance: mxl::MxlInstance, + reader: mxl::GrainReader, + flow_info: mxl::FlowInfo, +) -> Result<(), mxl::Error> { + let rate = flow_info.discrete_flow_info()?.grainRate; + let current_index = mxl_instance.get_current_index(&rate); + + info!("Grain rate: {}/{}", rate.numerator, rate.denominator); + + for index in current_index.. { + let grain_data = reader.get_complete_grain(index, READ_TIMEOUT)?; + info!( + "Index: {index} Grain data len: {:?}", + grain_data.payload.len() + ); + } + + Ok(()) +} + +fn read_samples( + mxl_instance: mxl::MxlInstance, + reader: mxl::SamplesReader, + flow_info: mxl::FlowInfo, + batch_size: Option, +) -> Result<(), mxl::Error> { + let flow_id = flow_info.common_flow_info().id().to_string(); + let sample_rate = flow_info.continuous_flow_info()?.sampleRate; + let continous_flow_info = flow_info.continuous_flow_info()?; + let batch_size = if let Some(batch_size) = batch_size { + if continous_flow_info.commitBatchSize != 0 + && batch_size != continous_flow_info.commitBatchSize as u64 + { + warn!( + "Writer batch size is set to {}, but sample batch size is provided, using the \ + latter.", + continous_flow_info.commitBatchSize + ); + } + batch_size as usize + } else { + if continous_flow_info.commitBatchSize == 0 { + let batch_size = (sample_rate.numerator / (100 * sample_rate.denominator)) as usize; + warn!( + "Writer batch size not available, using fallback value of {}.", + batch_size + ); + batch_size + } else { + continous_flow_info.commitBatchSize as usize + } + }; + let mut read_head = reader.get_info()?.continuous_flow_info()?.headIndex; + let mut read_head_valid_at = mxl_instance.get_time(); + info!( + "Will read from flow \"{flow_id}\" with sample rate {}/{}, using batches of size \ + {batch_size} samples, first batch ending at index {read_head}.", + sample_rate.numerator, sample_rate.denominator + ); + loop { + let samples_data = reader.get_samples(read_head, batch_size)?; + info!( + "Read samples for {} channel(s) at index {}.", + samples_data.num_of_channels(), + read_head + ); + if samples_data.num_of_channels() > 0 { + let channel_data = samples_data.channel_data(0)?; + info!( + "Buffer size for channel 0 is ({}, {}).", + channel_data.0.len(), + channel_data.1.len() + ); + } + // MXL currently does not have any samples reading mechanism which would wait for data to be + // available. + // We will just blindly assume that more data will be available when we need them. + // This, of course, does not have to be true, because the write batch size may be larger + // than our reading batch size. + let next_head = read_head + batch_size as u64; + let next_head_timestamp = mxl_instance.index_to_timestamp(next_head, &sample_rate)?; + let read_head_timestamp = mxl_instance.index_to_timestamp(read_head, &sample_rate)?; + let read_batch_duration = next_head_timestamp - read_head_timestamp; + let deadline = std::time::Instant::now() + READ_TIMEOUT; + loop { + read_head_valid_at += read_batch_duration; + let sleep_duration = + Duration::from_nanos(read_head_valid_at.saturating_sub(mxl_instance.get_time())); + info!("Will sleep for {:?}.", sleep_duration); + mxl_instance.sleep_for(sleep_duration); + if std::time::Instant::now() >= deadline { + warn!("Timeout while waiting for samples at index {}.", next_head); + return Err(mxl::Error::Timeout); + } + let available_head = reader.get_info()?.continuous_flow_info()?.headIndex; + if available_head >= next_head { + break; + } + } + read_head = next_head; + } +} diff --git a/rust/mxl/examples/flow-writer.rs b/rust/mxl/examples/flow-writer.rs new file mode 100644 index 00000000..7fe5ba13 --- /dev/null +++ b/rust/mxl/examples/flow-writer.rs @@ -0,0 +1,171 @@ +mod common; + +use clap::Parser; +use tracing::info; + +use mxl::config::get_mxf_so_path; + +#[derive(Debug, Parser)] +#[command(version = clap::crate_version!(), author = clap::crate_authors!())] +pub struct Opts { + /// The path to the shmem directory where the mxl domain is mapped. + #[arg(long)] + pub mxl_domain: String, + + /// The path to the configuration file describing the flow we want to write to. + #[arg(long)] + pub flow_config_file: String, + + /// The number of grains to write. If not specified, will run until stopped. + #[arg(long)] + pub grain_or_sample_count: Option, + + /// The number of samples to be written in one open samples call. Is only valid for "continuous" + /// flows. If not specified, will more or less fit 10 ms. + #[arg(long)] + pub sample_batch_size: Option, +} + +fn main() -> Result<(), mxl::Error> { + common::setup_logging(); + let opts: Opts = Opts::parse(); + + let mxl_api = mxl::load_api(get_mxf_so_path())?; + let mxl_instance = mxl::MxlInstance::new(mxl_api, &opts.mxl_domain, "")?; + let flow_def = mxl::tools::read_file(opts.flow_config_file.as_str()).map_err(|error| { + mxl::Error::Other(format!( + "Error while reading flow definition from \"{}\": {}", + &opts.flow_config_file, error + )) + })?; + let flow_info = mxl_instance.create_flow(flow_def.as_str(), None)?; + + if flow_info.is_discrete_flow() { + if opts.sample_batch_size.is_some() { + return Err(mxl::Error::Other( + "Sample batch size is only relevant for \"continuous\" flows.".to_owned(), + )); + } + write_grains(mxl_instance, flow_info, opts.grain_or_sample_count) + } else { + write_samples( + mxl_instance, + flow_info, + opts.grain_or_sample_count, + opts.sample_batch_size, + ) + } +} + +pub fn write_grains( + mxl_instance: mxl::MxlInstance, + flow_info: mxl::FlowInfo, + grain_count: Option, +) -> Result<(), mxl::Error> { + let flow_id = flow_info.common_flow_info().id().to_string(); + let grain_rate = flow_info.discrete_flow_info()?.grainRate; + let mut grain_index = mxl_instance.get_current_index(&grain_rate); + info!( + "Will write to flow \"{flow_id}\" with grain rate {}/{} starting from index {grain_index}.", + grain_rate.numerator, grain_rate.denominator + ); + let writer = mxl_instance + .create_flow_writer(flow_id.as_str())? + .to_grain_writer()?; + + let mut remaining_grains = grain_count; + loop { + if let Some(count) = remaining_grains { + if count == 0 { + break; + } + remaining_grains = Some(count - 1); + } + + let mut grain_writer_access = writer.open_grain(grain_index)?; + let payload = grain_writer_access.payload_mut(); + let payload_len = payload.len(); + for i in 0..payload_len { + payload[i] = ((i as u64 + grain_index) % 256) as u8; + } + grain_writer_access.commit(payload_len as u32)?; + + let timestamp = mxl_instance.index_to_timestamp(grain_index + 1, &grain_rate)?; + let sleep_duration = mxl_instance.get_duration_until_index(grain_index + 1, &grain_rate)?; + info!( + "Finished writing {payload_len} bytes into grain {grain_index}, will sleep for {:?} until timestamp {timestamp}.", + sleep_duration + ); + grain_index += 1; + mxl_instance.sleep_for(sleep_duration); + } + + info!("Finished writing requested number of grains, deleting the flow."); + writer.destroy()?; + mxl_instance.destroy_flow(flow_id.as_str())?; + Ok(()) +} + +pub fn write_samples( + mxl_instance: mxl::MxlInstance, + flow_info: mxl::FlowInfo, + sample_count: Option, + batch_size: Option, +) -> Result<(), mxl::Error> { + let flow_id = flow_info.common_flow_info().id().to_string(); + let sample_rate = flow_info.continuous_flow_info()?.sampleRate; + let batch_size = + batch_size.unwrap_or((sample_rate.numerator / (100 * sample_rate.denominator)) as u64); + let mut samples_index = mxl_instance.get_current_index(&sample_rate); + info!( + "Will write to flow \"{flow_id}\" with sample rate {}/{}, using batches of size {batch_size} samples, first batch ending at index {samples_index}.", + sample_rate.numerator, sample_rate.denominator + ); + let writer = mxl_instance + .create_flow_writer(flow_id.as_str())? + .to_samples_writer()?; + + let mut remaining_samples = sample_count; + loop { + if let Some(count) = remaining_samples { + if count == 0 { + break; + } + } + let samples_to_write = u64::min(batch_size, remaining_samples.unwrap_or(u64::MAX)); + if let Some(count) = remaining_samples { + remaining_samples = Some(count.saturating_sub(batch_size)); + } + + let mut samples_write_access = writer.open_samples(samples_index, batch_size as usize)?; + let mut writing_sample_index = samples_index - batch_size + 1; + for channel in 0..samples_write_access.channels() { + let (data_1, data_2) = samples_write_access.channel_data_mut(channel)?; + for i in 0..data_1.len() { + data_1[i] = (writing_sample_index % 256) as u8; + writing_sample_index += 1; + } + for i in 0..data_2.len() { + data_2[i] = (writing_sample_index % 256) as u8; + writing_sample_index += 1; + } + } + samples_write_access.commit()?; + + let timestamp = + mxl_instance.index_to_timestamp(samples_index + batch_size, &sample_rate)?; + let sleep_duration = + mxl_instance.get_duration_until_index(samples_index + batch_size, &sample_rate)?; + info!( + "Finished writing {samples_to_write} samples into batch ending with index {samples_index}, will sleep for {:?} until timestamp {timestamp}.", + sleep_duration + ); + samples_index += batch_size; + mxl_instance.sleep_for(sleep_duration); + } + + info!("Finished writing requested number of samples, deleting the flow."); + writer.destroy()?; + mxl_instance.destroy_flow(flow_id.as_str())?; + Ok(()) +} diff --git a/rust/mxl/src/api.rs b/rust/mxl/src/api.rs new file mode 100644 index 00000000..5cf81e9f --- /dev/null +++ b/rust/mxl/src/api.rs @@ -0,0 +1,146 @@ +use std::path::Path; + +use dlopen2::wrapper::{Container, WrapperApi}; + +use crate::Result; + +#[derive(WrapperApi)] +pub struct MxlApi { + #[dlopen2_name = "mxlGetVersion"] + mxl_get_version: + unsafe extern "C" fn(out_version: *mut mxl_sys::mxlVersionType) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlCreateInstance"] + mxl_create_instance: unsafe extern "C" fn( + in_mxlDomain: *const std::os::raw::c_char, + in_options: *const std::os::raw::c_char, + ) -> mxl_sys::mxlInstance, + #[dlopen2_name = "mxlGarbageCollectFlows"] + mxl_garbage_collect_flows: + unsafe extern "C" fn(in_instance: mxl_sys::mxlInstance) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlDestroyInstance"] + mxl_destroy_instance: + unsafe extern "C" fn(in_instance: mxl_sys::mxlInstance) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlCreateFlow"] + mxl_create_flow: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flowDef: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + info: *mut mxl_sys::FlowInfo, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlDestroyFlow"] + mxl_destroy_flow: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flowId: *const std::os::raw::c_char, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlCreateFlowReader"] + mxl_create_flow_reader: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flowId: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + reader: *mut mxl_sys::mxlFlowReader, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlReleaseFlowReader"] + mxl_release_flow_reader: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + reader: mxl_sys::mxlFlowReader, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlCreateFlowWriter"] + mxl_create_flow_writer: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flowId: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + writer: *mut mxl_sys::mxlFlowWriter, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlReleaseFlowWriter"] + mxl_release_flow_writer: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + writer: mxl_sys::mxlFlowWriter, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowReaderGetInfo"] + mxl_flow_reader_get_info: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + info: *mut mxl_sys::FlowInfo, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlFlowReaderGetGrain"] + mxl_flow_reader_get_grain: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + timeoutNs: u64, + grain: *mut mxl_sys::GrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowReaderGetGrainNonBlocking"] + mxl_flow_reader_get_grain_non_blocking: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + grain: *mut mxl_sys::GrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlFlowWriterOpenGrain"] + mxl_flow_writer_open_grain: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + index: u64, + grainInfo: *mut mxl_sys::GrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowWriterCancelGrain"] + mxl_flow_writer_cancel_grain: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowWriterCommitGrain"] + mxl_flow_writer_commit_grain: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + grain: *const mxl_sys::GrainInfo, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlFlowReaderGetSamples"] + mxl_flow_reader_get_samples: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + count: usize, + payloadBuffersSlices: *mut mxl_sys::WrappedMultiBufferSlice, + ) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlFlowWriterOpenSamples"] + mxl_flow_writer_open_samples: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + index: u64, + count: usize, + payloadBuffersSlices: *mut mxl_sys::MutableWrappedMultiBufferSlice, + ) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowWriterCancelSamples"] + mxl_flow_writer_cancel_samples: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + #[dlopen2_name = "mxlFlowWriterCommitSamples"] + mxl_flow_writer_commit_samples: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlGetCurrentIndex"] + mxl_get_current_index: unsafe extern "C" fn(editRate: *const mxl_sys::Rational) -> u64, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlGetNsUntilIndex"] + mxl_get_ns_until_index: + unsafe extern "C" fn(index: u64, editRate: *const mxl_sys::Rational) -> u64, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlTimestampToIndex"] + mxl_timestamp_to_index: + unsafe extern "C" fn(editRate: *const mxl_sys::Rational, timestamp: u64) -> u64, + #[allow(non_snake_case)] + #[dlopen2_name = "mxlIndexToTimestamp"] + mxl_index_to_timestamp: + unsafe extern "C" fn(editRate: *const mxl_sys::Rational, index: u64) -> u64, + #[dlopen2_name = "mxlSleepForNs"] + mxl_sleep_for_ns: unsafe extern "C" fn(ns: u64), + #[dlopen2_name = "mxlGetTime"] + mxl_get_time: unsafe extern "C" fn() -> u64, +} + +pub fn load_api(path_to_so_file: impl AsRef) -> Result> { + Ok(unsafe { Container::load(path_to_so_file.as_ref().as_os_str()) }?) +} diff --git a/rust/mxl/src/config.rs b/rust/mxl/src/config.rs new file mode 100644 index 00000000..f89891ae --- /dev/null +++ b/rust/mxl/src/config.rs @@ -0,0 +1,14 @@ +use std::str::FromStr; + +include!(concat!(env!("OUT_DIR"), "/constants.rs")); + +pub fn get_mxf_so_path() -> std::path::PathBuf { + std::path::PathBuf::from_str(MXL_BUILD_DIR) + .expect("build error: 'MXL_BUILD_DIR' is invalid") + .join("lib") + .join("libmxl.so") +} + +pub fn get_mxl_repo_root() -> std::path::PathBuf { + std::path::PathBuf::from_str(MXL_REPO_ROOT).expect("build error: 'MXL_REPO_ROOT' is invalid") +} diff --git a/rust/mxl/src/error.rs b/rust/mxl/src/error.rs new file mode 100644 index 00000000..a56a4aad --- /dev/null +++ b/rust/mxl/src/error.rs @@ -0,0 +1,50 @@ +pub type Result = core::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Unknown error: {0}")] + Unknown(mxl_sys::mxlStatus), + #[error("Flow not found")] + FlowNotFound, + #[error("Out of range - too late")] + OutOfRangeTooLate, + #[error("Out of range - too early")] + OutOfRangeTooEarly, + #[error("Invalid flow reader")] + InvalidFlowReader, + #[error("Invalid flow writer")] + InvalidFlowWriter, + #[error("Timeout")] + Timeout, + #[error("Invalid argument")] + InvalidArg, + #[error("Conflict")] + Conflict, + /// The error is not defined in the MXL API, but it is used to wrap other errors. + #[error("Other error: {0}")] + Other(String), + + #[error("dlopen: {0}")] + DlOpen(#[from] dlopen2::Error), + + #[error("Null string: {0}")] + NulString(#[from] std::ffi::NulError), +} + +impl Error { + pub fn from_status(status: mxl_sys::mxlStatus) -> Result<()> { + match status { + mxl_sys::MXL_STATUS_OK => Ok(()), + mxl_sys::MXL_ERR_UNKNOWN => Err(Error::Unknown(mxl_sys::MXL_ERR_UNKNOWN)), + mxl_sys::MXL_ERR_FLOW_NOT_FOUND => Err(Error::FlowNotFound), + mxl_sys::MXL_ERR_OUT_OF_RANGE_TOO_LATE => Err(Error::OutOfRangeTooLate), + mxl_sys::MXL_ERR_OUT_OF_RANGE_TOO_EARLY => Err(Error::OutOfRangeTooEarly), + mxl_sys::MXL_ERR_INVALID_FLOW_READER => Err(Error::InvalidFlowReader), + mxl_sys::MXL_ERR_INVALID_FLOW_WRITER => Err(Error::InvalidFlowWriter), + mxl_sys::MXL_ERR_TIMEOUT => Err(Error::Timeout), + mxl_sys::MXL_ERR_INVALID_ARG => Err(Error::InvalidArg), + mxl_sys::MXL_ERR_CONFLICT => Err(Error::Conflict), + other => Err(Error::Unknown(other)), + } + } +} diff --git a/rust/mxl/src/flow.rs b/rust/mxl/src/flow.rs new file mode 100644 index 00000000..d82e0eca --- /dev/null +++ b/rust/mxl/src/flow.rs @@ -0,0 +1,75 @@ +pub mod reader; +pub mod writer; + +use uuid::Uuid; + +use crate::{Error, Result}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DataFormat { + Unspecified, + Video, + Audio, + Data, + Mux, +} + +impl From for DataFormat { + fn from(value: u32) -> Self { + match value { + 0 => DataFormat::Unspecified, + mxl_sys::MXL_DATA_FORMAT_VIDEO => DataFormat::Video, + mxl_sys::MXL_DATA_FORMAT_AUDIO => DataFormat::Audio, + mxl_sys::MXL_DATA_FORMAT_DATA => DataFormat::Data, + mxl_sys::MXL_DATA_FORMAT_MUX => DataFormat::Mux, + _ => DataFormat::Unspecified, + } + } +} + +pub(crate) fn is_discrete_data_format(format: u32) -> bool { + // Check is based on mxlIsDiscreteDataFormat, which is inline, thus not accessible in mxl_sys. + format == mxl_sys::MXL_DATA_FORMAT_VIDEO || format == mxl_sys::MXL_DATA_FORMAT_DATA +} + +pub struct FlowInfo { + pub(crate) value: mxl_sys::FlowInfo, +} + +impl FlowInfo { + pub fn discrete_flow_info(&self) -> Result<&mxl_sys::DiscreteFlowInfo> { + if !is_discrete_data_format(self.value.common.format) { + return Err(Error::Other(format!( + "Flow format is {}, video or data required.", + self.value.common.format + ))); + } + Ok(unsafe { &self.value.__bindgen_anon_1.discrete }) + } + + pub fn continuous_flow_info(&self) -> Result<&mxl_sys::ContinuousFlowInfo> { + if is_discrete_data_format(self.value.common.format) { + return Err(Error::Other(format!( + "Flow format is {}, audio required.", + self.value.common.format + ))); + } + Ok(unsafe { &self.value.__bindgen_anon_1.continuous }) + } + + pub fn common_flow_info(&self) -> CommonFlowInfo<'_> { + CommonFlowInfo(&self.value.common) + } + + pub fn is_discrete_flow(&self) -> bool { + is_discrete_data_format(self.value.common.format) + } +} + +pub struct CommonFlowInfo<'a>(&'a mxl_sys::CommonFlowInfo); + +impl CommonFlowInfo<'_> { + pub fn id(&self) -> Uuid { + Uuid::from_bytes(self.0.id) + } +} diff --git a/rust/mxl/src/flow/reader.rs b/rust/mxl/src/flow/reader.rs new file mode 100644 index 00000000..a5bfd678 --- /dev/null +++ b/rust/mxl/src/flow/reader.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use crate::{ + DataFormat, Error, GrainReader, Result, SamplesReader, + flow::{FlowInfo, is_discrete_data_format}, + instance::InstanceContext, +}; + +pub struct MxlFlowReader { + context: Arc, + reader: mxl_sys::mxlFlowReader, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for MxlFlowReader {} + +pub(crate) fn get_flow_info( + context: &Arc, + reader: mxl_sys::mxlFlowReader, +) -> Result { + let mut flow_info: mxl_sys::FlowInfo = unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(context.api.mxl_flow_reader_get_info(reader, &mut flow_info))?; + } + Ok(FlowInfo { value: flow_info }) +} + +impl MxlFlowReader { + pub(crate) fn new(context: Arc, reader: mxl_sys::mxlFlowReader) -> Self { + Self { context, reader } + } + + pub fn get_info(&self) -> Result { + get_flow_info(&self.context, self.reader) + } + + pub fn to_grain_reader(mut self) -> Result { + let flow_type = self.get_info()?.value.common.format; + if !is_discrete_data_format(flow_type) { + return Err(Error::Other(format!( + "Cannot convert MxlFlowReader to GrainReader for continuous flow of type \"{:?}\".", + DataFormat::from(flow_type) + ))); + } + let result = GrainReader::new(self.context.clone(), self.reader); + self.reader = std::ptr::null_mut(); + Ok(result) + } + + pub fn to_samples_reader(mut self) -> Result { + let flow_type = self.get_info()?.value.common.format; + if is_discrete_data_format(flow_type) { + return Err(Error::Other(format!( + "Cannot convert MxlFlowReader to SamplesReader for discrete flow of type \"{:?}\".", + DataFormat::from(flow_type) + ))); + } + let result = SamplesReader::new(self.context.clone(), self.reader); + self.reader = std::ptr::null_mut(); + Ok(result) + } +} + +impl Drop for MxlFlowReader { + fn drop(&mut self) { + if !self.reader.is_null() + && let Err(err) = Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_reader(self.context.instance, self.reader) + }) + { + tracing::error!("Failed to release MXL flow reader: {:?}", err); + } + } +} diff --git a/rust/mxl/src/flow/writer.rs b/rust/mxl/src/flow/writer.rs new file mode 100644 index 00000000..250578db --- /dev/null +++ b/rust/mxl/src/flow/writer.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use crate::{ + DataFormat, Error, GrainWriter, Result, SamplesWriter, + flow::is_discrete_data_format, + instance::{InstanceContext, create_flow_reader}, +}; + +/// Generic MXL Flow Writer, which can be further used to build either the "discrete" (grain-based +/// data like video frames or meta) or "continuous" (audio samples) flow writers in MXL terminology. +pub struct MxlFlowWriter { + context: Arc, + writer: mxl_sys::mxlFlowWriter, + id: uuid::Uuid, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for MxlFlowWriter {} + +impl MxlFlowWriter { + pub(crate) fn new( + context: Arc, + writer: mxl_sys::mxlFlowWriter, + id: uuid::Uuid, + ) -> Self { + Self { + context, + writer, + id, + } + } + + pub fn to_grain_writer(mut self) -> Result { + let flow_type = self.get_flow_type()?; + if !is_discrete_data_format(flow_type) { + return Err(Error::Other(format!( + "Cannot convert MxlFlowWriter to GrainWriter for continuous flow of type \"{:?}\".", + DataFormat::from(flow_type) + ))); + } + let result = GrainWriter::new(self.context.clone(), self.writer); + self.writer = std::ptr::null_mut(); + Ok(result) + } + + pub fn to_samples_writer(mut self) -> Result { + let flow_type = self.get_flow_type()?; + if is_discrete_data_format(flow_type) { + return Err(Error::Other(format!( + "Cannot convert MxlFlowWriter to SamplesWriter for discrete flow of type \"{:?}\".", + DataFormat::from(flow_type) + ))); + } + let result = SamplesWriter::new(self.context.clone(), self.writer); + self.writer = std::ptr::null_mut(); + Ok(result) + } + + fn get_flow_type(&self) -> Result { + // This feels pretty ugly, but currently, the only way how to get a flow type in MXL is to + // use a reader. + let reader = create_flow_reader(&self.context, &self.id.to_string()).map_err(|error| { + Error::Other(format!( + "Error while creating flow reader to get the flow type: {error}" + )) + })?; + let flow_info = reader.get_info().map_err(|error| { + Error::Other(format!( + "Error while getting flow type from temporary reader: {error}" + )) + })?; + Ok(flow_info.value.common.format) + } +} + +impl Drop for MxlFlowWriter { + fn drop(&mut self) { + if !self.writer.is_null() + && let Err(err) = Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_writer(self.context.instance, self.writer) + }) + { + tracing::error!("Failed to release MXL flow writer: {:?}", err); + } + } +} diff --git a/rust/mxl/src/grain.rs b/rust/mxl/src/grain.rs new file mode 100644 index 00000000..9bfae8f0 --- /dev/null +++ b/rust/mxl/src/grain.rs @@ -0,0 +1,4 @@ +pub mod data; +pub mod reader; +pub mod write_access; +pub mod writer; diff --git a/rust/mxl/src/grain/data.rs b/rust/mxl/src/grain/data.rs new file mode 100644 index 00000000..fabdd135 --- /dev/null +++ b/rust/mxl/src/grain/data.rs @@ -0,0 +1,36 @@ +pub struct GrainData<'a> { + pub user_data: &'a [u8], + pub payload: &'a [u8], +} + +impl<'a> GrainData<'a> { + pub fn to_owned(&self) -> OwnedGrainData { + self.into() + } +} + +impl<'a> AsRef> for GrainData<'a> { + fn as_ref(&self) -> &GrainData<'a> { + self + } +} + +pub struct OwnedGrainData { + pub user_data: Vec, + pub payload: Vec, +} + +impl<'a> From<&GrainData<'a>> for OwnedGrainData { + fn from(value: &GrainData<'a>) -> Self { + Self { + user_data: value.user_data.to_vec(), + payload: value.payload.to_vec(), + } + } +} + +impl<'a> From> for OwnedGrainData { + fn from(value: GrainData<'a>) -> Self { + value.as_ref().into() + } +} diff --git a/rust/mxl/src/grain/reader.rs b/rust/mxl/src/grain/reader.rs new file mode 100644 index 00000000..227e3e8a --- /dev/null +++ b/rust/mxl/src/grain/reader.rs @@ -0,0 +1,97 @@ +use std::{sync::Arc, time::Duration}; + +use crate::{ + Error, GrainData, Result, + flow::{FlowInfo, reader::get_flow_info}, + instance::InstanceContext, +}; + +pub struct GrainReader { + context: Arc, + reader: mxl_sys::mxlFlowReader, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for GrainReader {} + +impl GrainReader { + pub(crate) fn new(context: Arc, reader: mxl_sys::mxlFlowReader) -> Self { + Self { context, reader } + } + + pub fn destroy(mut self) -> Result<()> { + self.destroy_inner() + } + + pub fn get_info(&self) -> Result { + get_flow_info(&self.context, self.reader) + } + + pub fn get_complete_grain<'a>( + &'a self, + index: u64, + timeout: Duration, + ) -> Result> { + let mut grain_info: mxl_sys::GrainInfo = unsafe { std::mem::zeroed() }; + let mut payload_ptr: *mut u8 = std::ptr::null_mut(); + let timeout_ns = timeout.as_nanos() as u64; + loop { + unsafe { + Error::from_status(self.context.api.mxl_flow_reader_get_grain( + self.reader, + index, + timeout_ns, + &mut grain_info, + &mut payload_ptr, + ))?; + } + if grain_info.commitedSize != grain_info.grainSize { + // We don't need partial grains. Wait for the grain to be complete. + continue; + } + if payload_ptr.is_null() { + return Err(Error::Other(format!( + "Failed to get grain payload for index {index}.", + ))); + } + break; + } + + // SAFETY + // We know that the lifetime is as long as the flow, so it is at least self's lifetime. + // It may happen that the buffer is overwritten by a subsequent write, but it is safe. + let user_data: &'a [u8] = + unsafe { std::mem::transmute::<&[u8], &'a [u8]>(&grain_info.userData) }; + + let payload = + unsafe { std::slice::from_raw_parts(payload_ptr, grain_info.grainSize as usize) }; + + Ok(GrainData { user_data, payload }) + } + + fn destroy_inner(&mut self) -> Result<()> { + if self.reader.is_null() { + return Err(Error::InvalidArg); + } + + let mut reader = std::ptr::null_mut(); + std::mem::swap(&mut self.reader, &mut reader); + + Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_reader(self.context.instance, reader) + }) + } +} + +impl Drop for GrainReader { + fn drop(&mut self) { + if !self.reader.is_null() + && let Err(err) = self.destroy_inner() + { + tracing::error!("Failed to release MXL flow reader (discrete): {:?}", err); + } + } +} diff --git a/rust/mxl/src/grain/write_access.rs b/rust/mxl/src/grain/write_access.rs new file mode 100644 index 00000000..04447aef --- /dev/null +++ b/rust/mxl/src/grain/write_access.rs @@ -0,0 +1,96 @@ +use std::{marker::PhantomData, sync::Arc}; + +use tracing::error; + +use crate::{Error, Result, instance::InstanceContext}; + +/// RAII grain writing session +/// +/// Automatically cancels the grain if not explicitly committed. +pub struct GrainWriteAccess<'a> { + context: Arc, + writer: mxl_sys::mxlFlowWriter, + grain_info: mxl_sys::GrainInfo, + payload_ptr: *mut u8, + /// Serves as a flag to know whether to cancel the grain on drop. + committed_or_canceled: bool, + phantom: PhantomData<&'a ()>, +} + +impl<'a> GrainWriteAccess<'a> { + pub(crate) fn new( + context: Arc, + writer: mxl_sys::mxlFlowWriter, + grain_info: mxl_sys::GrainInfo, + payload_ptr: *mut u8, + ) -> Self { + Self { + context, + writer, + grain_info, + payload_ptr, + committed_or_canceled: false, + phantom: Default::default(), + } + } + + pub fn payload_mut(&mut self) -> &mut [u8] { + unsafe { + std::slice::from_raw_parts_mut(self.payload_ptr, self.grain_info.grainSize as usize) + } + } + + pub fn user_data_mut(&mut self) -> &mut [u8] { + &mut self.grain_info.userData + } + + pub fn max_size(&self) -> u32 { + self.grain_info.grainSize + } + + pub fn committed_size(&self) -> u32 { + self.grain_info.commitedSize + } + + pub fn commit(mut self, commited_size: u32) -> Result<()> { + self.committed_or_canceled = true; + + if commited_size > self.grain_info.grainSize { + return Err(Error::Other(format!( + "Commited size {} cannot exceed grain size {}.", + commited_size, self.grain_info.grainSize + ))); + } + self.grain_info.commitedSize = commited_size; + + unsafe { + Error::from_status( + self.context + .api + .mxl_flow_writer_commit_grain(self.writer, &self.grain_info), + ) + } + } + + /// Please note that the behavior of canceling a grain writing is dependent on the behavior + /// implemented in MXL itself. Particularly, if grain data has been mutated and then writing + /// canceled, mutation will most likely stay in place, only head won't be updated, and readers + /// notified. + pub fn cancel(mut self) -> Result<()> { + self.committed_or_canceled = true; + + unsafe { Error::from_status(self.context.api.mxl_flow_writer_cancel_grain(self.writer)) } + } +} + +impl<'a> Drop for GrainWriteAccess<'a> { + fn drop(&mut self) { + if !self.committed_or_canceled + && let Err(error) = unsafe { + Error::from_status(self.context.api.mxl_flow_writer_cancel_grain(self.writer)) + } + { + error!("Failed to cancel grain write on drop: {:?}", error); + } + } +} diff --git a/rust/mxl/src/grain/writer.rs b/rust/mxl/src/grain/writer.rs new file mode 100644 index 00000000..a2399e0b --- /dev/null +++ b/rust/mxl/src/grain/writer.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use super::write_access::GrainWriteAccess; + +use crate::{Error, Result, instance::InstanceContext}; + +/// MXL Flow Writer for discrete flows (grain-based data like video frames) +pub struct GrainWriter { + context: Arc, + writer: mxl_sys::mxlFlowWriter, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for GrainWriter {} + +impl GrainWriter { + pub(crate) fn new(context: Arc, writer: mxl_sys::mxlFlowWriter) -> Self { + Self { context, writer } + } + + pub fn destroy(mut self) -> Result<()> { + self.destroy_inner() + } + + /// The current MXL implementation states a TODO to allow multiple grains to be edited at the + /// same time. For this reason, there is no protection on the Rust level against trying to open + /// multiple grains. If the TODO ever gets removed, it may be worth considering pattern where + /// opening grain would consume the writer and then return it back on commit or cancel. + pub fn open_grain<'a>(&'a self, index: u64) -> Result> { + let mut grain_info: mxl_sys::GrainInfo = unsafe { std::mem::zeroed() }; + let mut payload_ptr: *mut u8 = std::ptr::null_mut(); + unsafe { + Error::from_status(self.context.api.mxl_flow_writer_open_grain( + self.writer, + index, + &mut grain_info, + &mut payload_ptr, + ))?; + } + + if payload_ptr.is_null() { + return Err(Error::Other(format!( + "Failed to open grain payload for index {index}.", + ))); + } + + Ok(GrainWriteAccess::new( + self.context.clone(), + self.writer, + grain_info, + payload_ptr, + )) + } + + fn destroy_inner(&mut self) -> Result<()> { + if self.writer.is_null() { + return Err(Error::InvalidArg); + } + + let mut writer = std::ptr::null_mut(); + std::mem::swap(&mut self.writer, &mut writer); + + Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_writer(self.context.instance, writer) + }) + } +} + +impl Drop for GrainWriter { + fn drop(&mut self) { + if !self.writer.is_null() + && let Err(err) = self.destroy_inner() + { + tracing::error!("Failed to release MXL flow writer (discrete): {:?}", err); + } + } +} diff --git a/rust/mxl/src/instance.rs b/rust/mxl/src/instance.rs new file mode 100644 index 00000000..95861b5e --- /dev/null +++ b/rust/mxl/src/instance.rs @@ -0,0 +1,209 @@ +use std::{ffi::CString, sync::Arc}; + +use dlopen2::wrapper::Container; + +use crate::{Error, FlowInfo, MxlApi, MxlFlowReader, MxlFlowWriter, Result}; + +/// This struct stores the context that is shared by all objects. +/// It is separated out from `MxlInstance` so that it can be cloned +/// and other objects' lifetimes be decoupled from the MxlInstance +/// itself. +pub(crate) struct InstanceContext { + pub(crate) api: Container, + pub(crate) instance: mxl_sys::mxlInstance, +} + +// Allow sharing the context across threads and tasks freely. +// This is safe because the MXL API is supposed to be thread-safe at the +// instance level (careful, not at the reader / writer level). +unsafe impl Send for InstanceContext {} +unsafe impl Sync for InstanceContext {} + +impl InstanceContext { + /// This function forces the destruction of the MXL instance. + /// It is meant mainly for testing purposes. + pub fn destroy(mut self) -> Result<()> { + unsafe { + let mut instance = std::ptr::null_mut(); + std::mem::swap(&mut self.instance, &mut instance); + self.api.mxl_destroy_instance(self.instance) + }; + Ok(()) + } +} + +impl Drop for InstanceContext { + fn drop(&mut self) { + if !self.instance.is_null() { + unsafe { self.api.mxl_destroy_instance(self.instance) }; + } + } +} + +pub(crate) fn create_flow_reader( + context: &Arc, + flow_id: &str, +) -> Result { + let flow_id = CString::new(flow_id)?; + let options = CString::new("")?; + let mut reader: mxl_sys::mxlFlowReader = std::ptr::null_mut(); + unsafe { + Error::from_status(context.api.mxl_create_flow_reader( + context.instance, + flow_id.as_ptr(), + options.as_ptr(), + &mut reader, + ))?; + } + if reader.is_null() { + return Err(Error::Other("Failed to create flow reader.".to_string())); + } + Ok(MxlFlowReader::new(context.clone(), reader)) +} + +#[derive(Clone)] +pub struct MxlInstance { + context: Arc, +} + +impl MxlInstance { + pub fn new(api: Container, domain: &str, options: &str) -> Result { + let instance = unsafe { + api.mxl_create_instance( + CString::new(domain)?.as_ptr(), + CString::new(options)?.as_ptr(), + ) + }; + if instance.is_null() { + Err(Error::Other("Failed to create MXL instance.".to_string())) + } else { + let context = Arc::new(InstanceContext { api, instance }); + Ok(Self { context }) + } + } + + pub fn create_flow_reader(&self, flow_id: &str) -> Result { + create_flow_reader(&self.context, flow_id) + } + + pub fn create_flow_writer(&self, flow_id: &str) -> Result { + let uuid = uuid::Uuid::parse_str(flow_id) + .map_err(|_| Error::Other("Invalid flow ID format.".to_string()))?; + let flow_id = CString::new(flow_id)?; + let options = CString::new("")?; + let mut writer: mxl_sys::mxlFlowWriter = std::ptr::null_mut(); + unsafe { + Error::from_status(self.context.api.mxl_create_flow_writer( + self.context.instance, + flow_id.as_ptr(), + options.as_ptr(), + &mut writer, + ))?; + } + if writer.is_null() { + return Err(Error::Other("Failed to create flow writer.".to_string())); + } + Ok(MxlFlowWriter::new(self.context.clone(), writer, uuid)) + } + + /// For now, we provide direct access to the MXL API for creating and + /// destroying flows. Maybe it would be worth to provide RAII wrapper... + /// Instead? As well? + pub fn create_flow(&self, flow_def: &str, options: Option<&str>) -> Result { + let flow_def = CString::new(flow_def)?; + let options = CString::new(options.unwrap_or(""))?; + let mut info = std::mem::MaybeUninit::::uninit(); + + unsafe { + Error::from_status(self.context.api.mxl_create_flow( + self.context.instance, + flow_def.as_ptr(), + options.as_ptr(), + info.as_mut_ptr(), + ))?; + } + + let info = unsafe { info.assume_init() }; + Ok(FlowInfo { value: info }) + } + + /// See `create_flow` for more info. + pub fn destroy_flow(&self, flow_id: &str) -> Result<()> { + let flow_id = CString::new(flow_id)?; + unsafe { + Error::from_status( + self.context + .api + .mxl_destroy_flow(self.context.instance, flow_id.as_ptr()), + )?; + } + Ok(()) + } + + pub fn get_current_index(&self, rational: &mxl_sys::Rational) -> u64 { + unsafe { self.context.api.mxl_get_current_index(rational) } + } + + pub fn get_duration_until_index( + &self, + index: u64, + rate: &mxl_sys::Rational, + ) -> Result { + let duration_ns = unsafe { self.context.api.mxl_get_ns_until_index(index, rate) }; + if duration_ns == u64::MAX { + Err(Error::Other(format!( + "Failed to get duration until index, invalid rate {}/{}.", + rate.numerator, rate.denominator + ))) + } else { + Ok(std::time::Duration::from_nanos(duration_ns)) + } + } + + /// TODO: Make timestamp a strong type. + pub fn timestamp_to_index(&self, timestamp: u64, rate: &mxl_sys::Rational) -> Result { + let index = unsafe { self.context.api.mxl_timestamp_to_index(rate, timestamp) }; + if index == u64::MAX { + Err(Error::Other(format!( + "Failed to convert timestamp to index, invalid rate {}/{}.", + rate.numerator, rate.denominator + ))) + } else { + Ok(index) + } + } + + pub fn index_to_timestamp(&self, index: u64, rate: &mxl_sys::Rational) -> Result { + let timestamp = unsafe { self.context.api.mxl_index_to_timestamp(rate, index) }; + if timestamp == u64::MAX { + Err(Error::Other(format!( + "Failed to convert index to timestamp, invalid rate {}/{}.", + rate.numerator, rate.denominator + ))) + } else { + Ok(timestamp) + } + } + + pub fn sleep_for(&self, duration: std::time::Duration) { + unsafe { + self.context + .api + .mxl_sleep_for_ns(duration.as_nanos() as u64) + } + } + + pub fn get_time(&self) -> u64 { + unsafe { self.context.api.mxl_get_time() } + } + + /// This function forces the destruction of the MXL instance. + /// It is meant mainly for testing purposes. + /// The caller must ensure that no other objects are using the MXL instance when this function + /// is called. + pub fn destroy(self) -> Result<()> { + let context = Arc::into_inner(self.context) + .ok_or_else(|| Error::Other("Instance is still in use.".to_string()))?; + context.destroy() + } +} diff --git a/rust/mxl/src/lib.rs b/rust/mxl/src/lib.rs new file mode 100644 index 00000000..b2ce1e5e --- /dev/null +++ b/rust/mxl/src/lib.rs @@ -0,0 +1,20 @@ +mod api; +mod error; +mod flow; +mod grain; +mod instance; +mod samples; + +pub mod config; +pub mod tools; + +pub use api::{MxlApi, load_api}; +pub use error::{Error, Result}; +pub use flow::{reader::MxlFlowReader, writer::MxlFlowWriter, *}; +pub use grain::{ + data::*, reader::GrainReader, write_access::GrainWriteAccess, writer::GrainWriter, +}; +pub use instance::MxlInstance; +pub use samples::{ + data::*, reader::SamplesReader, write_access::SamplesWriteAccess, writer::SamplesWriter, +}; diff --git a/rust/mxl/src/samples.rs b/rust/mxl/src/samples.rs new file mode 100644 index 00000000..9bfae8f0 --- /dev/null +++ b/rust/mxl/src/samples.rs @@ -0,0 +1,4 @@ +pub mod data; +pub mod reader; +pub mod write_access; +pub mod writer; diff --git a/rust/mxl/src/samples/data.rs b/rust/mxl/src/samples/data.rs new file mode 100644 index 00000000..4c079690 --- /dev/null +++ b/rust/mxl/src/samples/data.rs @@ -0,0 +1,75 @@ +use std::marker::PhantomData; + +use crate::Error; + +pub struct SamplesData<'a> { + buffer_slice: mxl_sys::WrappedMultiBufferSlice, + phantom: PhantomData<&'a ()>, +} + +impl<'a> SamplesData<'a> { + pub(crate) fn new(buffer_slice: mxl_sys::WrappedMultiBufferSlice) -> Self { + Self { + buffer_slice, + phantom: Default::default(), + } + } + + pub fn num_of_channels(&self) -> usize { + self.buffer_slice.count + } + + pub fn channel_data(&self, channel: usize) -> crate::Result<(&[u8], &[u8])> { + if channel >= self.buffer_slice.count { + return Err(Error::InvalidArg); + } + unsafe { + let ptr_1 = (self.buffer_slice.base.fragments[0].pointer as *const u8) + .add(self.buffer_slice.stride * channel); + let size_1 = self.buffer_slice.base.fragments[0].size; + let ptr_2 = (self.buffer_slice.base.fragments[1].pointer as *const u8) + .add(self.buffer_slice.stride * channel); + let size_2 = self.buffer_slice.base.fragments[1].size; + Ok(( + std::slice::from_raw_parts(ptr_1, size_1), + std::slice::from_raw_parts(ptr_2, size_2), + )) + } + } + + pub fn to_owned(&self) -> OwnedSamplesData { + self.into() + } +} + +impl<'a> AsRef> for SamplesData<'a> { + fn as_ref(&self) -> &SamplesData<'a> { + self + } +} + +pub struct OwnedSamplesData { + /// Data belonging to each of the channels. + pub payload: Vec>, +} + +impl<'a> From<&SamplesData<'a>> for OwnedSamplesData { + fn from(value: &SamplesData<'a>) -> Self { + let mut payload = Vec::with_capacity(value.buffer_slice.count); + for channel in 0..value.buffer_slice.count { + // The following unwrap is safe because the channel index always stays in the valid range. + let (data_1, data_2) = value.channel_data(channel).unwrap(); + let mut channel_payload = Vec::with_capacity(data_1.len() + data_2.len()); + channel_payload.extend(data_1); + channel_payload.extend(data_2); + payload.push(channel_payload); + } + Self { payload } + } +} + +impl<'a> From> for OwnedSamplesData { + fn from(value: SamplesData<'a>) -> Self { + value.as_ref().into() + } +} diff --git a/rust/mxl/src/samples/reader.rs b/rust/mxl/src/samples/reader.rs new file mode 100644 index 00000000..e6f75dcd --- /dev/null +++ b/rust/mxl/src/samples/reader.rs @@ -0,0 +1,68 @@ +use std::sync::Arc; + +use crate::{ + Error, Result, SamplesData, + flow::{FlowInfo, reader::get_flow_info}, + instance::InstanceContext, +}; + +pub struct SamplesReader { + context: Arc, + reader: mxl_sys::mxlFlowReader, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for SamplesReader {} + +impl SamplesReader { + pub(crate) fn new(context: Arc, reader: mxl_sys::mxlFlowReader) -> Self { + Self { context, reader } + } + + pub fn destroy(mut self) -> Result<()> { + self.destroy_inner() + } + + pub fn get_info(&self) -> Result { + get_flow_info(&self.context, self.reader) + } + + pub fn get_samples(&self, index: u64, count: usize) -> Result> { + let mut buffer_slice: mxl_sys::WrappedMultiBufferSlice = unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(self.context.api.mxl_flow_reader_get_samples( + self.reader, + index, + count, + &mut buffer_slice, + ))?; + } + Ok(SamplesData::new(buffer_slice)) + } + + fn destroy_inner(&mut self) -> Result<()> { + if self.reader.is_null() { + return Err(Error::InvalidArg); + } + + let mut reader = std::ptr::null_mut(); + std::mem::swap(&mut self.reader, &mut reader); + + Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_reader(self.context.instance, reader) + }) + } +} + +impl Drop for SamplesReader { + fn drop(&mut self) { + if !self.reader.is_null() + && let Err(err) = self.destroy_inner() + { + tracing::error!("Failed to release MXL flow reader (continuous): {:?}", err); + } + } +} diff --git a/rust/mxl/src/samples/write_access.rs b/rust/mxl/src/samples/write_access.rs new file mode 100644 index 00000000..bbb57ce1 --- /dev/null +++ b/rust/mxl/src/samples/write_access.rs @@ -0,0 +1,93 @@ +use std::{marker::PhantomData, sync::Arc}; + +use tracing::error; + +use crate::{Error, instance::InstanceContext}; + +/// RAII samples writing session +/// +/// Automatically cancels the samples if not explicitly committed. +/// +/// The data may be split into 2 different buffer slices in case of a wrapped ring. Provides access +/// either directly to the slices or to individual samples by index inside the batch. +pub struct SamplesWriteAccess<'a> { + context: Arc, + writer: mxl_sys::mxlFlowWriter, + buffer_slice: mxl_sys::MutableWrappedMultiBufferSlice, + /// Serves as a flag to know whether to cancel the samples on drop. + committed_or_canceled: bool, + phantom: PhantomData<&'a ()>, +} + +impl<'a> SamplesWriteAccess<'a> { + pub(crate) fn new( + context: Arc, + writer: mxl_sys::mxlFlowWriter, + buffer_slice: mxl_sys::MutableWrappedMultiBufferSlice, + ) -> Self { + Self { + context, + writer, + buffer_slice, + committed_or_canceled: false, + phantom: PhantomData, + } + } + + pub fn commit(mut self) -> crate::Result<()> { + self.committed_or_canceled = true; + + unsafe { Error::from_status(self.context.api.mxl_flow_writer_commit_samples(self.writer)) } + } + + /// Please note that the behavior of canceling samples writing is dependent on the behavior + /// implemented in MXL itself. Particularly, if samples data have been mutated and then writing + /// canceled, mutation will most likely stay in place, only head won't be updated, and readers + /// notified. + pub fn cancel(mut self) -> crate::Result<()> { + self.committed_or_canceled = true; + + unsafe { Error::from_status(self.context.api.mxl_flow_writer_cancel_samples(self.writer)) } + } + + pub fn channels(&self) -> usize { + self.buffer_slice.count + } + + /// Provides direct access to buffer of the given channel. The access is split into two slices + /// to cover cases when the ring is not continuous. + /// + /// Currently, we provide just raw bytes access. Probably we should provide some sample-based + /// access and some index-based access (where we hide the complexity of 2 slices) as well? + /// + /// Samples are f32? + pub fn channel_data_mut(&mut self, channel: usize) -> crate::Result<(&mut [u8], &mut [u8])> { + if channel >= self.buffer_slice.count { + return Err(Error::InvalidArg); + } + unsafe { + let ptr_1 = (self.buffer_slice.base.fragments[0].pointer as *mut u8) + .add(self.buffer_slice.stride * channel); + let size_1 = self.buffer_slice.base.fragments[0].size; + let ptr_2 = (self.buffer_slice.base.fragments[1].pointer as *mut u8) + .add(self.buffer_slice.stride * channel); + let size_2 = self.buffer_slice.base.fragments[1].size; + Ok(( + std::slice::from_raw_parts_mut(ptr_1, size_1), + std::slice::from_raw_parts_mut(ptr_2, size_2), + )) + } + } +} + +impl<'a> Drop for SamplesWriteAccess<'a> { + fn drop(&mut self) { + if !self.committed_or_canceled + && let Err(error) = unsafe { + Error::from_status(self.context.api.mxl_flow_writer_cancel_samples(self.writer)) + } + { + error!("Failed to cancel grain write on drop: {:?}", error); + } + } +} diff --git a/rust/mxl/src/samples/writer.rs b/rust/mxl/src/samples/writer.rs new file mode 100644 index 00000000..4c0193f5 --- /dev/null +++ b/rust/mxl/src/samples/writer.rs @@ -0,0 +1,66 @@ +use std::sync::Arc; + +use crate::{Error, Result, SamplesWriteAccess, instance::InstanceContext}; + +/// MXL Flow Writer for continuous flows (samples-based data like audio) +pub struct SamplesWriter { + context: Arc, + writer: mxl_sys::mxlFlowWriter, +} + +/// The MXL readers and writers are not thread-safe, so we do not implement `Sync` for them, but +/// there is no reason to not implement `Send`. +unsafe impl Send for SamplesWriter {} + +impl SamplesWriter { + pub(crate) fn new(context: Arc, writer: mxl_sys::mxlFlowWriter) -> Self { + Self { context, writer } + } + + pub fn destroy(mut self) -> Result<()> { + self.destroy_inner() + } + + pub fn open_samples<'a>(&'a self, index: u64, count: usize) -> Result> { + let mut buffer_slice: mxl_sys::MutableWrappedMultiBufferSlice = + unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(self.context.api.mxl_flow_writer_open_samples( + self.writer, + index, + count, + &mut buffer_slice, + ))?; + } + Ok(SamplesWriteAccess::new( + self.context.clone(), + self.writer, + buffer_slice, + )) + } + + fn destroy_inner(&mut self) -> Result<()> { + if self.writer.is_null() { + return Err(Error::InvalidArg); + } + + let mut writer = std::ptr::null_mut(); + std::mem::swap(&mut self.writer, &mut writer); + + Error::from_status(unsafe { + self.context + .api + .mxl_release_flow_writer(self.context.instance, writer) + }) + } +} + +impl Drop for SamplesWriter { + fn drop(&mut self) { + if !self.writer.is_null() + && let Err(err) = self.destroy_inner() + { + tracing::error!("Failed to release MXL flow writer (continuous): {:?}", err); + } + } +} diff --git a/rust/mxl/src/tools.rs b/rust/mxl/src/tools.rs new file mode 100644 index 00000000..312af218 --- /dev/null +++ b/rust/mxl/src/tools.rs @@ -0,0 +1,8 @@ +pub fn read_file(file_path: impl AsRef) -> Result { + use std::io::Read; + + let mut file = std::fs::File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) +} diff --git a/rust/mxl/tests/basic_tests.rs b/rust/mxl/tests/basic_tests.rs new file mode 100644 index 00000000..7242077c --- /dev/null +++ b/rust/mxl/tests/basic_tests.rs @@ -0,0 +1,106 @@ +/// Tests of the basic low level synchronous API. +/// +/// The tests now require an MXL library of a specific name to be present in the system. This should +/// change in the future. For now, feel free to just edit the path to your library. +use std::time::Duration; + +use mxl::{MxlInstance, OwnedGrainData, OwnedSamplesData, config::get_mxf_so_path}; +use tracing::info; + +static LOG_ONCE: std::sync::Once = std::sync::Once::new(); + +fn setup_empty_domain(test: &str) -> String { + let result = format!("/dev/shm/mxl_rust_unit_tests_domain_{}", test); + if std::path::Path::new(result.as_str()).exists() { + std::fs::remove_dir_all(result.as_str()) + .expect("Failed to remove existing test domain directory"); + } + std::fs::create_dir_all(result.as_str()).expect("Failed to create test domain directory"); + result +} + +fn setup_test(test: &str) -> mxl::MxlInstance { + // Set up the logging to use the RUST_LOG environment variable and if not present, print INFO + // and higher. + LOG_ONCE.call_once(|| { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + }); + + let mxl_api = mxl::load_api(get_mxf_so_path()).unwrap(); + let domain = setup_empty_domain(test); + mxl::MxlInstance::new(mxl_api, &domain, "").unwrap() +} + +fn prepare_flow_info>( + mxl_instance: &MxlInstance, + path: P, +) -> mxl::FlowInfo { + let flow_config_file = mxl::config::get_mxl_repo_root().join(path); + let flow_def = mxl::tools::read_file(flow_config_file.as_path()) + .map_err(|error| { + mxl::Error::Other(format!( + "Error while reading flow definition from \"{}\": {}", + flow_config_file.display(), + error + )) + }) + .unwrap(); + mxl_instance.create_flow(flow_def.as_str(), None).unwrap() +} + +#[test] +fn basic_mxl_grain_writing_reading() { + let mxl_instance = setup_test("grains"); + let flow_info = prepare_flow_info(&mxl_instance, "lib/tests/data/v210_flow.json"); + let flow_id = flow_info.common_flow_info().id().to_string(); + let flow_writer = mxl_instance.create_flow_writer(flow_id.as_str()).unwrap(); + let grain_writer = flow_writer.to_grain_writer().unwrap(); + let flow_reader = mxl_instance.create_flow_reader(flow_id.as_str()).unwrap(); + let grain_reader = flow_reader.to_grain_reader().unwrap(); + let rate = flow_info.discrete_flow_info().unwrap().grainRate; + let current_index = mxl_instance.get_current_index(&rate); + let grain_write_access = grain_writer.open_grain(current_index).unwrap(); + let grain_size = grain_write_access.max_size(); + grain_write_access.commit(grain_size).unwrap(); + let grain_data = grain_reader + .get_complete_grain(current_index, Duration::from_secs(5)) + .unwrap(); + let grain_data: OwnedGrainData = grain_data.into(); + info!("Grain data len: {:?}", grain_data.payload.len()); + grain_reader.destroy().unwrap(); + grain_writer.destroy().unwrap(); + mxl_instance.destroy_flow(flow_id.as_str()).unwrap(); + mxl_instance.destroy().unwrap(); +} + +#[test] +fn basic_mxl_samples_writing_reading() { + let mxl_instance = setup_test("samples"); + let flow_info = prepare_flow_info(&mxl_instance, "lib/tests/data/audio_flow.json"); + let flow_id = flow_info.common_flow_info().id().to_string(); + let flow_writer = mxl_instance.create_flow_writer(flow_id.as_str()).unwrap(); + let samples_writer = flow_writer.to_samples_writer().unwrap(); + let flow_reader = mxl_instance.create_flow_reader(flow_id.as_str()).unwrap(); + let samples_reader = flow_reader.to_samples_reader().unwrap(); + let rate = flow_info.continuous_flow_info().unwrap().sampleRate; + let current_index = mxl_instance.get_current_index(&rate); + let samples_write_access = samples_writer.open_samples(current_index, 42).unwrap(); + samples_write_access.commit().unwrap(); + let samples_data = samples_reader.get_samples(current_index, 42).unwrap(); + let samples_data: OwnedSamplesData = samples_data.into(); + info!( + "Samples data contains {} channels(s), channel 0 has {} byte(s).", + samples_data.payload.len(), + samples_data.payload[0].len() + ); + samples_reader.destroy().unwrap(); + samples_writer.destroy().unwrap(); + mxl_instance.destroy_flow(flow_id.as_str()).unwrap(); + mxl_instance.destroy().unwrap(); +}