diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bc0d66ad..590416bc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -91,4 +91,5 @@ RUN git clone https://github.com/microsoft/vcpkg \ ENV VCPKG_ROOT="/home/${USERNAME}/vcpkg" -RUN rustup default 1.88.0 +COPY ./scripts/ /tmp/scripts/ +RUN /tmp/scripts/common/rust/install-rust.sh diff --git a/.devcontainer/Dockerfile.almalinux b/.devcontainer/Dockerfile.almalinux index e571e2f5..3a141343 100644 --- a/.devcontainer/Dockerfile.almalinux +++ b/.devcontainer/Dockerfile.almalinux @@ -78,4 +78,5 @@ RUN git clone https://github.com/microsoft/vcpkg \ ENV VCPKG_ROOT=/home/$USERNAME/vcpkg -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=1.88.0 +COPY ./scripts/ /tmp/scripts/ +RUN /tmp/scripts/common/rust/install-rust.sh diff --git a/.devcontainer/Dockerfile.amazonlinux b/.devcontainer/Dockerfile.amazonlinux index c7d5418d..1c0d95dd 100644 --- a/.devcontainer/Dockerfile.amazonlinux +++ b/.devcontainer/Dockerfile.amazonlinux @@ -77,4 +77,5 @@ RUN git clone https://github.com/microsoft/vcpkg \ ENV VCPKG_ROOT=/home/$USERNAME/vcpkg -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=1.88.0 +COPY ./scripts/ /tmp/scripts/ +RUN /tmp/scripts/common/rust/install-rust.sh diff --git a/.devcontainer/Dockerfile.debiantrixie b/.devcontainer/Dockerfile.debiantrixie index 879fd2fa..9132ad2b 100644 --- a/.devcontainer/Dockerfile.debiantrixie +++ b/.devcontainer/Dockerfile.debiantrixie @@ -86,4 +86,5 @@ RUN git clone https://github.com/microsoft/vcpkg \ ENV VCPKG_ROOT="/home/${USERNAME}/vcpkg" -RUN rustup default 1.88.0 +COPY ./scripts/ /tmp/scripts/ +RUN /tmp/scripts/common/rust/install-rust.sh diff --git a/.devcontainer/Dockerfile.ubuntu-legacy b/.devcontainer/Dockerfile.ubuntu-legacy index ed6e75b3..731e373f 100644 --- a/.devcontainer/Dockerfile.ubuntu-legacy +++ b/.devcontainer/Dockerfile.ubuntu-legacy @@ -104,4 +104,5 @@ ENV VCPKG_ROOT=/home/$USERNAME/vcpkg # Make the selected clang version the default alternative RUN sudo /tmp/scripts/debian/register-clang-version.sh ${CLANG_VERSION} 100 -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=1.88.0 +COPY ./scripts/ /tmp/scripts/ +RUN /tmp/scripts/common/rust/install-rust.sh diff --git a/.devcontainer/almalinux/devcontainer.json b/.devcontainer/almalinux/devcontainer.json index c80d44b2..72c16e5c 100644 --- a/.devcontainer/almalinux/devcontainer.json +++ b/.devcontainer/almalinux/devcontainer.json @@ -28,7 +28,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -37,7 +38,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -50,4 +54,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/amazonlinux/devcontainer.json b/.devcontainer/amazonlinux/devcontainer.json index 7d456894..9a12fd37 100644 --- a/.devcontainer/amazonlinux/devcontainer.json +++ b/.devcontainer/amazonlinux/devcontainer.json @@ -32,7 +32,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -41,7 +42,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -51,4 +55,4 @@ "DISPLAY": "${localEnv:DISPLAY}" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/debiantrixie/devcontainer.json b/.devcontainer/debiantrixie/devcontainer.json index bb5b17b3..c3085b13 100644 --- a/.devcontainer/debiantrixie/devcontainer.json +++ b/.devcontainer/debiantrixie/devcontainer.json @@ -28,7 +28,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -37,7 +38,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -50,4 +54,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/scripts/common/rust/install-rust.sh b/.devcontainer/scripts/common/rust/install-rust.sh new file mode 100755 index 00000000..253388cd --- /dev/null +++ b/.devcontainer/scripts/common/rust/install-rust.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +RUST_VERSION=1.90.0 + +if command -v rustup > /dev/null 2>&1; then + rustup default $RUST_VERSION +else + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=$RUST_VERSION + . "$HOME/.cargo/env" +fi + +# Install cargo binstall +curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + +cargo binstall cargo-audit --locked +cargo binstall cargo-outdated --locked + +# udeps requires the nightly compiler, so using machete (at least for now) +# cargo binstall cargo-udeps --locked +cargo binstall cargo-machete --locked + +cargo binstall cargo-deny --locked +cargo binstall cargo-nextest --locked diff --git a/.devcontainer/ubuntu20/devcontainer.json b/.devcontainer/ubuntu20/devcontainer.json index b3321a83..cb10c067 100644 --- a/.devcontainer/ubuntu20/devcontainer.json +++ b/.devcontainer/ubuntu20/devcontainer.json @@ -30,7 +30,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -39,7 +40,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -52,4 +56,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/ubuntu22/devcontainer.json b/.devcontainer/ubuntu22/devcontainer.json index 57c7dbd3..fdc217ec 100644 --- a/.devcontainer/ubuntu22/devcontainer.json +++ b/.devcontainer/ubuntu22/devcontainer.json @@ -30,7 +30,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -39,7 +40,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -52,4 +56,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/ubuntu24/devcontainer.json b/.devcontainer/ubuntu24/devcontainer.json index 9777cf7f..57789ef2 100644 --- a/.devcontainer/ubuntu24/devcontainer.json +++ b/.devcontainer/ubuntu24/devcontainer.json @@ -30,7 +30,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -39,7 +40,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -52,4 +56,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.devcontainer/ubuntu25/devcontainer.json b/.devcontainer/ubuntu25/devcontainer.json index 1343790e..d69f9d4b 100644 --- a/.devcontainer/ubuntu25/devcontainer.json +++ b/.devcontainer/ubuntu25/devcontainer.json @@ -30,7 +30,8 @@ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", "xaver.clang-format", - "bierner.markdown-mermaid" + "bierner.markdown-mermaid", + "rust-lang.rust-analyzer" ], "settings": { "C_Cpp.intelliSenseEngine": "disabled", @@ -39,7 +40,10 @@ "editor.formatOnSave": true, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" - } + }, + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/rust/Cargo.toml" + ] } } }, @@ -52,4 +56,4 @@ "gpu": "optional" }, "remoteUser": "devcontainer" -} \ No newline at end of file +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6361d01..fe97e30b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,6 +87,9 @@ jobs: mkdir -p ${{ github.workspace }}/install chmod 777 ${{ github.workspace }}/install chmod g+s ${{ github.workspace }}/install + mkdir -p ${{ github.workspace }}/rust/target + chmod 777 ${{ github.workspace }}/rust/target + chmod g+s ${{ github.workspace }}/rust/target - name: Build Docker image if: steps.check-image.outputs.exists == 'false' @@ -173,6 +176,52 @@ jobs: ctest --output-junit test-results.xml " + - name: Cache Cargo registry + 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: Check the Rust bindings + env: + RUSTFLAGS: "-Dwarnings" + run: | + docker run --mount src=${{ github.workspace }},target=/workspace/mxl,type=bind \ + -i mxl_build_container_with_source \ + bash -c " + cd /workspace/mxl/rust && \ + cargo fmt -- --check && \ + cargo clippy --all-targets --all-features --locked -- -D warnings && \ + cargo audit && \ + cargo outdated && \ + cargo machete && \ + cargo deny check all + " + + - name: Build Rust bindings + run: | + docker run --mount src=${{ github.workspace }},target=/workspace/mxl,type=bind \ + -i mxl_build_container_with_source \ + bash -c " + cd /workspace/mxl/rust && \ + cargo build --release --all-targets --locked + " + + - name: Test the Rust bindings + run: | + docker run --mount src=${{ github.workspace }},target=/workspace/mxl,type=bind \ + -i mxl_build_container_with_source \ + bash -c " + cd /workspace/mxl/rust && \ + cargo nextest run --release --locked + " + - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: always() 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 16cfe775..48249512 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,10 @@ else() string(APPEND mxl_VERSION ".0") endif() +option(BUILD_DOCS "Build the docs" ON) +option(BUILD_TESTS "Build the tests" ON) +option(BUILD_TOOLS "Build the tools" ON) + project(mxl VERSION ${mxl_VERSION} LANGUAGES CXX C @@ -56,42 +60,46 @@ if(APPLE) endif() add_subdirectory(lib) -add_subdirectory(tools) add_subdirectory(utils) +if (BUILD_TOOLS) + add_subdirectory(tools) +endif() -find_package(Doxygen) +if(BUILD_DOCS) + find_package(Doxygen) -if(DOXYGEN_FOUND) - include(FetchContent) + if(DOXYGEN_FOUND) + include(FetchContent) - FetchContent_Declare( - doxygen-awesome-css - URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip - ) - FetchContent_MakeAvailable(doxygen-awesome-css) + FetchContent_Declare( + doxygen-awesome-css + URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip + ) + FetchContent_MakeAvailable(doxygen-awesome-css) - # Save the location the files were cloned into - # This allows us to get the path to doxygen-awesome.css - FetchContent_GetProperties(doxygen-awesome-css SOURCE_DIR AWESOME_CSS_DIR) + # Save the location the files were cloned into + # This allows us to get the path to doxygen-awesome.css + FetchContent_GetProperties(doxygen-awesome-css SOURCE_DIR AWESOME_CSS_DIR) - # Generate the Doxyfile - set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) - set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) - configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) + # Generate the Doxyfile + set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) + set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) + configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY) - set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/docs") + set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/docs") - add_custom_target(doc - COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMENT "Generating API documentation with Doxygen" - BYPRODUCTS "${DOXYGEN_OUTPUT_DIR}/html/index.html" - VERBATIM - ) + add_custom_target(doc + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating API documentation with Doxygen" + BYPRODUCTS "${DOXYGEN_OUTPUT_DIR}/html/index.html" + VERBATIM + ) - install(DIRECTORY "${CMAKE_BINARY_DIR}/docs/html" - DESTINATION share/doc/mxl - FILES_MATCHING PATTERN "*") + install(DIRECTORY "${CMAKE_BINARY_DIR}/docs/html" + DESTINATION share/doc/mxl + FILES_MATCHING PATTERN "*") + endif() endif() if(EXISTS "/etc/os-release") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 2cb3cda4..3a2c24f5 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -87,7 +87,9 @@ if (MXL_ENABLE_FABRICS_OFI) add_subdirectory(fabrics) endif () -add_subdirectory(tests) +if (BUILD_TESTS) + add_subdirectory(tests) +endif() # Install targets install(TARGETS mxl EXPORT ${PROJECT_NAME}-targets diff --git a/rust/.config/nextest.toml b/rust/.config/nextest.toml new file mode 100644 index 00000000..d7db88de --- /dev/null +++ b/rust/.config/nextest.toml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +[profile.ci] +fail-fast = false + +[profile.ci.junit] +path = "junit.xml" diff --git a/rust/.gitattributes b/rust/.gitattributes new file mode 100644 index 00000000..265e6dd7 --- /dev/null +++ b/rust/.gitattributes @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +Cargo.lock -diff diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 00000000..91712988 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +.DS_Store +.idea +.vscode +target diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..9ea4d3b8 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,741 @@ +# 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 0.53.2", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[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.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[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", + "regex-syntax", +] + +[[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", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "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-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/rust/Cargo.lock.license b/rust/Cargo.lock.license new file mode 100644 index 00000000..e8d14bb3 --- /dev/null +++ b/rust/Cargo.lock.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..3a030638 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +[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.20", 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..d2e6bc81 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,30 @@ + + +# 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/deny.toml b/rust/deny.toml new file mode 100644 index 00000000..d8a4044e --- /dev/null +++ b/rust/deny.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Unicode-3.0", + "BSD-3-Clause", + "ISC" +] 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..eab9efe9 --- /dev/null +++ b/rust/flake.nix @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +{ + 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..bbf8a55d --- /dev/null +++ b/rust/mxl-sys/.gitignore @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +mxl/version.h diff --git a/rust/mxl-sys/Cargo.toml b/rust/mxl-sys/Cargo.toml new file mode 100644 index 00000000..1da42252 --- /dev/null +++ b/rust/mxl-sys/Cargo.toml @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "mxl-sys" +edition.workspace = true +publish.workspace = true +version.workspace = true +license.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..4154ed03 --- /dev/null +++ b/rust/mxl-sys/build.rs @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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"; + +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 out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let build_version_dir = out_dir.join("include").to_string_lossy().to_string(); + + includes_dirs.push(build_version_dir); + + // Rebuild if any file in lib/ changes + let lib_root = repo_root.join("lib"); + println!("cargo:rerun-if-changed={}", lib_root.display()); + + let dst = cmake::Config::new(repo_root) + .generator("Ninja") + .configure_arg("--preset") + .configure_arg(BUILD_VARIANT) + .configure_arg("-B") + .configure_arg(out_dir.join("build")) + .define("BUILD_DOCS", "OFF") + .define("BUILD_TESTS", "OFF") + .define("BUILD_TOOLS", "OFF") + .build(); + + println!("cargo:rustc-link-search={}", dst.join("lib").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..d82df112 --- /dev/null +++ b/rust/mxl-sys/src/lib.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(missing_docs)] +#![allow(rustdoc::broken_intra_doc_links)] +#![allow(rustdoc::invalid_html_tags)] +// 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..d0357dc4 --- /dev/null +++ b/rust/mxl-sys/tests/simple_test.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +#[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..44b4bd77 --- /dev/null +++ b/rust/mxl-sys/wrapper-with-version-h.h @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +#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..e6fb4216 --- /dev/null +++ b/rust/mxl-sys/wrapper-without-version-h.h @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +#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..ca640489 --- /dev/null +++ b/rust/mxl/Cargo.toml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "mxl" +edition.workspace = true +publish.workspace = true +version.workspace = true +license.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..4451cc68 --- /dev/null +++ b/rust/mxl/build.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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..e37eb881 --- /dev/null +++ b/rust/mxl/examples/common/mod.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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..d2290788 --- /dev/null +++ b/rust/mxl/examples/flow-reader.rs @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use std::time::Duration; + +use clap::Parser; +use mxl::config::get_mxl_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_mxl_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 common_flow_info = flow_info.common_flow_info(); + let batch_size = if let Some(batch_size) = batch_size { + if common_flow_info.max_commit_batch_size_hint() != 0 + && batch_size != common_flow_info.max_commit_batch_size_hint() as u64 + { + warn!( + "Writer batch size is set to {}, but sample batch size is provided, using the \ + latter.", + common_flow_info.max_commit_batch_size_hint() + ); + } + batch_size as usize + } else if common_flow_info.max_commit_batch_size_hint() == 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 { + common_flow_info.max_commit_batch_size_hint() 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..6f38ffa3 --- /dev/null +++ b/rust/mxl/examples/flow-writer.rs @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use clap::Parser; +use tracing::info; + +use mxl::config::get_mxl_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_mxl_so_path())?; + let mxl_instance = mxl::MxlInstance::new(mxl_api, &opts.mxl_domain, "")?; + let flow_def = std::fs::read_to_string(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 total_slices = grain_writer_access.total_slices(); + let payload = grain_writer_access.payload_mut(); + let payload_len = payload.len(); + for (i, byte) in payload.iter_mut().enumerate() { + *byte = ((i as u64 + grain_index) % 256) as u8; + } + grain_writer_access.commit(total_slices)?; + + 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 ({total_slices} slices) 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 + && 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 sample in data_1.iter_mut() { + *sample = (writing_sample_index % 256) as u8; + writing_sample_index += 1; + } + for sample in data_2.iter_mut() { + *sample = (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..35f70254 --- /dev/null +++ b/rust/mxl/src/api.rs @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +use std::{path::Path, sync::Arc}; + +use dlopen2::wrapper::{Container, WrapperApi}; + +use crate::Result; + +#[derive(WrapperApi)] +pub struct MxlApi { + #[dlopen2_name = "mxlGetVersion"] + get_version: + unsafe extern "C" fn(out_version: *mut mxl_sys::mxlVersionType) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlCreateInstance"] + create_instance: unsafe extern "C" fn( + in_mxl_domain: *const std::os::raw::c_char, + in_options: *const std::os::raw::c_char, + ) -> mxl_sys::mxlInstance, + + #[dlopen2_name = "mxlGarbageCollectFlows"] + garbage_collect_flows: + unsafe extern "C" fn(in_instance: mxl_sys::mxlInstance) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlDestroyInstance"] + destroy_instance: unsafe extern "C" fn(in_instance: mxl_sys::mxlInstance) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlCreateFlow"] + create_flow: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flow_def: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + info: *mut mxl_sys::mxlFlowInfo, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlDestroyFlow"] + destroy_flow: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flow_id: *const std::os::raw::c_char, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlGetFlowDef"] + get_flow_def: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flow_id: *const ::std::os::raw::c_char, + buffer: *mut ::std::os::raw::c_char, + buffer_size: *mut usize, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlCreateFlowReader"] + create_flow_reader: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flow_id: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + reader: *mut mxl_sys::mxlFlowReader, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlReleaseFlowReader"] + release_flow_reader: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + reader: mxl_sys::mxlFlowReader, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlCreateFlowWriter"] + create_flow_writer: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + flow_id: *const std::os::raw::c_char, + options: *const std::os::raw::c_char, + writer: *mut mxl_sys::mxlFlowWriter, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlReleaseFlowWriter"] + release_flow_writer: unsafe extern "C" fn( + instance: mxl_sys::mxlInstance, + writer: mxl_sys::mxlFlowWriter, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowReaderGetInfo"] + flow_reader_get_info: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + info: *mut mxl_sys::mxlFlowInfo, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowReaderGetGrain"] + flow_reader_get_grain: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + timeout_ns: u64, + grain: *mut mxl_sys::mxlGrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowReaderGetGrainNonBlocking"] + flow_reader_get_grain_non_blocking: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + grain: *mut mxl_sys::mxlGrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterOpenGrain"] + flow_writer_open_grain: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + index: u64, + grain_info: *mut mxl_sys::mxlGrainInfo, + payload: *mut *mut u8, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterCancelGrain"] + flow_writer_cancel_grain: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterCommitGrain"] + flow_writer_commit_grain: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + grain: *const mxl_sys::mxlGrainInfo, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowReaderGetSamples"] + flow_reader_get_samples: unsafe extern "C" fn( + reader: mxl_sys::mxlFlowReader, + index: u64, + count: usize, + payload_buffers_slices: *mut mxl_sys::mxlWrappedMultiBufferSlice, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterOpenSamples"] + flow_writer_open_samples: unsafe extern "C" fn( + writer: mxl_sys::mxlFlowWriter, + index: u64, + count: usize, + payload_buffers_slices: *mut mxl_sys::mxlMutableWrappedMultiBufferSlice, + ) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterCancelSamples"] + flow_writer_cancel_samples: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlFlowWriterCommitSamples"] + flow_writer_commit_samples: + unsafe extern "C" fn(writer: mxl_sys::mxlFlowWriter) -> mxl_sys::mxlStatus, + + #[dlopen2_name = "mxlGetCurrentIndex"] + get_current_index: unsafe extern "C" fn(edit_rate: *const mxl_sys::mxlRational) -> u64, + + #[dlopen2_name = "mxlGetNsUntilIndex"] + get_ns_until_index: + unsafe extern "C" fn(index: u64, edit_rate: *const mxl_sys::mxlRational) -> u64, + + #[dlopen2_name = "mxlTimestampToIndex"] + timestamp_to_index: + unsafe extern "C" fn(edit_rate: *const mxl_sys::mxlRational, timestamp: u64) -> u64, + + #[dlopen2_name = "mxlIndexToTimestamp"] + index_to_timestamp: + unsafe extern "C" fn(edit_rate: *const mxl_sys::mxlRational, index: u64) -> u64, + + #[dlopen2_name = "mxlSleepForNs"] + sleep_for_ns: unsafe extern "C" fn(ns: u64), + + #[dlopen2_name = "mxlGetTime"] + get_time: unsafe extern "C" fn() -> u64, +} + +pub type MxlApiHandle = Arc>; + +pub fn load_api(path_to_so_file: impl AsRef) -> Result { + Ok(Arc::new(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..b80fd0c2 --- /dev/null +++ b/rust/mxl/src/config.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +include!(concat!(env!("OUT_DIR"), "/constants.rs")); + +#[cfg(not(feature = "mxl-not-built"))] +pub fn get_mxl_so_path() -> std::path::PathBuf { + // The mxl-sys build script ensures that the build directory is in the library path + // so we can just return the library name here. + "libmxl.so".into() +} + +#[cfg(feature = "mxl-not-built")] +pub fn get_mxl_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..d5372feb --- /dev/null +++ b/rust/mxl/src/error.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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..acfc5bd7 --- /dev/null +++ b/rust/mxl/src/flow.rs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlFlowInfo, +} + +impl FlowInfo { + pub fn discrete_flow_info(&self) -> Result<&mxl_sys::mxlDiscreteFlowInfo> { + 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::mxlContinuousFlowInfo> { + 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::mxlCommonFlowInfo); + +impl CommonFlowInfo<'_> { + pub fn id(&self) -> Uuid { + Uuid::from_bytes(self.0.id) + } + + pub fn max_commit_batch_size_hint(&self) -> u32 { + self.0.maxCommitBatchSizeHint + } +} diff --git a/rust/mxl/src/flow/reader.rs b/rust/mxl/src/flow/reader.rs new file mode 100644 index 00000000..522f0eed --- /dev/null +++ b/rust/mxl/src/flow/reader.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use crate::{ + DataFormat, Error, GrainReader, Result, SamplesReader, + flow::{FlowInfo, is_discrete_data_format}, + instance::InstanceContext, +}; + +pub struct FlowReader { + 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 FlowReader {} + +pub(crate) fn get_flow_info( + context: &Arc, + reader: mxl_sys::mxlFlowReader, +) -> Result { + let mut flow_info: mxl_sys::mxlFlowInfo = unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(context.api.flow_reader_get_info(reader, &mut flow_info))?; + } + Ok(FlowInfo { value: flow_info }) +} + +impl FlowReader { + 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 FlowReader { + fn drop(&mut self) { + if !self.reader.is_null() + && let Err(err) = Error::from_status(unsafe { + self.context + .api + .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..1b80de58 --- /dev/null +++ b/rust/mxl/src/flow/writer.rs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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 FlowWriter { + 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 FlowWriter {} + +impl FlowWriter { + 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 FlowWriter { + fn drop(&mut self) { + if !self.writer.is_null() + && let Err(err) = Error::from_status(unsafe { + self.context + .api + .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..2205933c --- /dev/null +++ b/rust/mxl/src/grain.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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..fca910fa --- /dev/null +++ b/rust/mxl/src/grain/data.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +pub struct GrainData<'a> { + /// The grain payload. This may be a partial payload if the grain is not complete. + /// The length of this slice is given by `commitedSize` in `mxlGrainInfo`. + pub payload: &'a [u8], + + /// The total size of the grain payload, which may be larger than `payload.len()` if the grain is partial. + pub total_size: usize, +} + +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 payload: Vec, +} + +impl<'a> From<&GrainData<'a>> for OwnedGrainData { + fn from(value: &GrainData<'a>) -> Self { + Self { + 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..ee07c32e --- /dev/null +++ b/rust/mxl/src/grain/reader.rs @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlGrainInfo = 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.flow_reader_get_grain( + self.reader, + index, + timeout_ns, + &mut grain_info, + &mut payload_ptr, + ))?; + } + if grain_info.validSlices != grain_info.totalSlices { + // 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 payload = + unsafe { std::slice::from_raw_parts(payload_ptr, grain_info.grainSize as usize) }; + + Ok(GrainData { + payload, + total_size: grain_info.grainSize as usize, + }) + } + + /// Non-blocking version of `get_complete_grain`. If the grain is not available, returns an error. + /// If the grain is partial, it is returned as is and the payload length will be smaller than the total grain size. + pub fn get_grain_non_blocking<'a>(&'a self, index: u64) -> Result> { + let mut grain_info: mxl_sys::mxlGrainInfo = unsafe { std::mem::zeroed() }; + let mut payload_ptr: *mut u8 = std::ptr::null_mut(); + unsafe { + Error::from_status(self.context.api.flow_reader_get_grain_non_blocking( + self.reader, + index, + &mut grain_info, + &mut payload_ptr, + ))?; + } + + if payload_ptr.is_null() { + return Err(Error::Other(format!( + "Failed to get grain payload for index {index}.", + ))); + } + + // 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 payload = + unsafe { std::slice::from_raw_parts(payload_ptr, grain_info.grainSize as usize) }; + + Ok(GrainData { + payload, + total_size: grain_info.grainSize as usize, + }) + } + + 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 + .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..7fd6aa63 --- /dev/null +++ b/rust/mxl/src/grain/write_access.rs @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlGrainInfo, + 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::mxlGrainInfo, + 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 max_size(&self) -> u32 { + self.grain_info.grainSize + } + + pub fn total_slices(&self) -> u16 { + self.grain_info.totalSlices + } + + pub fn commit(mut self, valid_slices: u16) -> Result<()> { + self.committed_or_canceled = true; + + if valid_slices > self.grain_info.totalSlices { + return Err(Error::Other(format!( + "Valid slices {} cannot exceed total slices {}.", + valid_slices, self.grain_info.totalSlices + ))); + } + self.grain_info.validSlices = valid_slices; + + unsafe { + Error::from_status( + self.context + .api + .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.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.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..e312627e --- /dev/null +++ b/rust/mxl/src/grain/writer.rs @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlGrainInfo = unsafe { std::mem::zeroed() }; + let mut payload_ptr: *mut u8 = std::ptr::null_mut(); + unsafe { + Error::from_status(self.context.api.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 + .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..ae9a9110 --- /dev/null +++ b/rust/mxl/src/instance.rs @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ffi::CString, sync::Arc}; + +use crate::{Error, FlowInfo, FlowReader, FlowWriter, Result, api::MxlApiHandle}; + +/// 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: MxlApiHandle, + 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.destroy_instance(self.instance) + }; + Ok(()) + } +} + +impl Drop for InstanceContext { + fn drop(&mut self) { + if !self.instance.is_null() { + unsafe { self.api.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.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(FlowReader::new(context.clone(), reader)) +} + +#[derive(Clone)] +pub struct MxlInstance { + context: Arc, +} + +impl MxlInstance { + pub fn new(api: MxlApiHandle, domain: &str, options: &str) -> Result { + let instance = unsafe { + api.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.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(FlowWriter::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.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 + .destroy_flow(self.context.instance, flow_id.as_ptr()), + )?; + } + Ok(()) + } + + pub fn get_flow_def(&self, flow_id: &str) -> Result { + let flow_id = CString::new(flow_id)?; + const INITIAL_BUFFER_SIZE: usize = 4096; + let mut buffer: Vec = vec![0; INITIAL_BUFFER_SIZE]; + let mut buffer_size = INITIAL_BUFFER_SIZE; + + let status = unsafe { + self.context.api.get_flow_def( + self.context.instance, + flow_id.as_ptr(), + buffer.as_mut_ptr() as *mut std::os::raw::c_char, + &mut buffer_size, + ) + }; + + if status == mxl_sys::MXL_ERR_INVALID_ARG && buffer_size > INITIAL_BUFFER_SIZE { + buffer = vec![0; buffer_size]; + unsafe { + Error::from_status(self.context.api.get_flow_def( + self.context.instance, + flow_id.as_ptr(), + buffer.as_mut_ptr() as *mut std::os::raw::c_char, + &mut buffer_size, + ))?; + } + } else { + Error::from_status(status)?; + } + + if buffer_size > 0 && buffer[buffer_size - 1] == 0 { + buffer_size -= 1; + } + buffer.truncate(buffer_size); + + String::from_utf8(buffer) + .map_err(|_| Error::Other("Invalid UTF-8 in flow definition".to_string())) + } + + pub fn get_current_index(&self, rational: &mxl_sys::mxlRational) -> u64 { + unsafe { self.context.api.get_current_index(rational) } + } + + pub fn get_duration_until_index( + &self, + index: u64, + rate: &mxl_sys::mxlRational, + ) -> Result { + let duration_ns = unsafe { self.context.api.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::mxlRational) -> Result { + let index = unsafe { self.context.api.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::mxlRational) -> Result { + let timestamp = unsafe { self.context.api.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.sleep_for_ns(duration.as_nanos() as u64) } + } + + pub fn get_time(&self) -> u64 { + unsafe { self.context.api.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..d33137f7 --- /dev/null +++ b/rust/mxl/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +mod api; +mod error; +mod flow; +mod grain; +mod instance; +mod samples; + +pub mod config; + +pub use api::{MxlApi, load_api}; +pub use error::{Error, Result}; +pub use flow::{reader::FlowReader, writer::FlowWriter, *}; +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..2205933c --- /dev/null +++ b/rust/mxl/src/samples.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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..08e17603 --- /dev/null +++ b/rust/mxl/src/samples/data.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::Error; + +pub struct SamplesData<'a> { + buffer_slice: mxl_sys::mxlWrappedMultiBufferSlice, + phantom: PhantomData<&'a ()>, +} + +impl<'a> SamplesData<'a> { + pub(crate) fn new(buffer_slice: mxl_sys::mxlWrappedMultiBufferSlice) -> 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..4ad387c0 --- /dev/null +++ b/rust/mxl/src/samples/reader.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlWrappedMultiBufferSlice = unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(self.context.api.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 + .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..53672036 --- /dev/null +++ b/rust/mxl/src/samples/write_access.rs @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlMutableWrappedMultiBufferSlice, + /// 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::mxlMutableWrappedMultiBufferSlice, + ) -> 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.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.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.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..c8f6996b --- /dev/null +++ b/rust/mxl/src/samples/writer.rs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +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::mxlMutableWrappedMultiBufferSlice = + unsafe { std::mem::zeroed() }; + unsafe { + Error::from_status(self.context.api.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 + .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/tests/basic_tests.rs b/rust/mxl/tests/basic_tests.rs new file mode 100644 index 00000000..aea4edcd --- /dev/null +++ b/rust/mxl/tests/basic_tests.rs @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2025 2025 Contributors to the Media eXchange Layer project. +// SPDX-License-Identifier: Apache-2.0 + +/// 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_mxl_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_mxl_so_path()).unwrap(); + let domain = setup_empty_domain(test); + mxl::MxlInstance::new(mxl_api, &domain, "").unwrap() +} + +fn read_flow_def>(path: P) -> String { + let flow_config_file = mxl::config::get_mxl_repo_root().join(path); + + std::fs::read_to_string(flow_config_file.as_path()) + .map_err(|error| { + mxl::Error::Other(format!( + "Error while reading flow definition from \"{}\": {}", + flow_config_file.display(), + error + )) + }) + .unwrap() +} + +fn prepare_flow_info>( + mxl_instance: &MxlInstance, + path: P, +) -> mxl::FlowInfo { + let flow_def = read_flow_def(path); + 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 total_slices = grain_write_access.total_slices(); + grain_write_access.commit(total_slices).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(); +} + +#[test] +fn get_flow_def() { + let mxl_instance = setup_test("flow_def"); + let flow_def = read_flow_def("lib/tests/data/v210_flow.json"); + let flow_info = mxl_instance.create_flow(flow_def.as_str(), None).unwrap(); + let flow_id = flow_info.common_flow_info().id().to_string(); + let retrieved_flow_def = mxl_instance.get_flow_def(flow_id.as_str()).unwrap(); + assert_eq!(flow_def, retrieved_flow_def); + mxl_instance.destroy_flow(flow_id.as_str()).unwrap(); + mxl_instance.destroy().unwrap(); +}