├── .clippy.toml ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build-reusable.yml │ ├── ci.yml │ ├── release.yml │ └── scheduled.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── cli │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── Dockerfile │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── package.sh │ ├── src │ │ └── main.rs │ └── tests │ │ ├── integration.rs │ │ ├── snapshots │ │ ├── error-drop-fn.svg │ │ ├── error-processing.svg │ │ └── with-tracing.svg │ │ └── test.wasm ├── lib │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── src │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── processor │ │ │ ├── error.rs │ │ │ ├── functions.rs │ │ │ ├── mod.rs │ │ │ └── state.rs │ │ └── signature.rs │ └── tests │ │ ├── modules │ │ ├── simple-no-inline.wast │ │ └── simple.wast │ │ ├── processor.rs │ │ └── version_match.rs └── macro │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── src │ ├── externref.rs │ └── lib.rs │ └── tests │ ├── integration.rs │ ├── ui │ ├── fn_with_bogus_export_name.rs │ ├── fn_with_bogus_export_name.stderr │ ├── item_with_bogus_abi.rs │ ├── item_with_bogus_abi.stderr │ ├── item_without_abi.rs │ ├── item_without_abi.stderr │ ├── module_with_bogus_link_name.rs │ ├── module_with_bogus_link_name.stderr │ ├── module_with_bogus_name.rs │ ├── module_with_bogus_name.stderr │ ├── module_without_name.rs │ ├── module_without_name.stderr │ ├── unsupported_item.rs │ ├── unsupported_item.stderr │ ├── variadic_fn.rs │ └── variadic_fn.stderr │ └── version_match.rs ├── deny.toml └── e2e-tests ├── Cargo.toml ├── README.md ├── src └── lib.rs └── tests └── integration ├── compile.rs └── main.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | # Minimum supported Rust version. Should be consistent with CI and mentions 2 | # in crate READMEs. 3 | msrv = "1.76" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Do not send the `target` dir to the Docker builder 2 | target 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.rs] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this template for reporting issues 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug report 10 | 11 | 12 | 13 | ### Steps to reproduce 14 | 15 | 16 | 17 | ### Expected behavior 18 | 19 | 20 | 21 | ### Environment 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Use this template to request features 4 | title: '' 5 | labels: feat 6 | assignees: '' 7 | --- 8 | 9 | ## Feature request 10 | 11 | 12 | 13 | ### Why? 14 | 15 | 16 | 17 | ### Alternatives 18 | 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "03:00" 8 | groups: 9 | dev-dependencies: 10 | dependency-type: "development" 11 | minor-changes: 12 | update-types: 13 | - "minor" 14 | - "patch" 15 | open-pull-requests-limit: 10 16 | assignees: 17 | - slowli 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | 4 | 5 | 6 | 7 | ## Why? 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/build-reusable.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | rust_version: 7 | type: string 8 | description: Rust version to use in the build 9 | required: false 10 | default: stable 11 | 12 | env: 13 | binaryen: version_110 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install Rust 23 | uses: dtolnay/rust-toolchain@master 24 | with: 25 | toolchain: ${{ inputs.rust_version }} 26 | targets: wasm32-unknown-unknown 27 | components: rustfmt, clippy 28 | - name: Install wasm-opt 29 | run: | 30 | wget -q -O binaryen.tar.gz https://github.com/WebAssembly/binaryen/releases/download/$binaryen/binaryen-$binaryen-x86_64-linux.tar.gz && \ 31 | tar xf binaryen.tar.gz && \ 32 | sudo install "binaryen-$binaryen/bin/wasm-opt" /usr/local/bin 33 | - name: Install cargo-deny 34 | uses: baptiste0928/cargo-install@v2 35 | with: 36 | crate: cargo-deny 37 | version: "^0.16" 38 | 39 | - name: Cache cargo build 40 | uses: actions/cache@v4 41 | with: 42 | path: target 43 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 44 | restore-keys: ${{ runner.os }}-cargo 45 | 46 | - name: Format 47 | run: cargo fmt --all -- --check --config imports_granularity=Crate --config group_imports=StdExternalCrate 48 | 49 | - name: Clippy 50 | run: cargo clippy --workspace --all-features --all-targets -- -D warnings 51 | - name: Clippy (no features) 52 | run: cargo clippy -p externref --no-default-features --lib -- -D warnings 53 | - name: Clippy (processor) 54 | run: cargo clippy -p externref --no-default-features --features=processor --lib -- -D warnings 55 | 56 | - name: Check dependencies 57 | run: cargo deny --all-features check 58 | 59 | # Build the E2E crate first to speed up its testing. 60 | - name: Build E2E test crate 61 | run: cargo build -p externref-test --lib --target wasm32-unknown-unknown --profile wasm 62 | 63 | - name: Run tests 64 | run: cargo test --workspace --all-features --all-targets 65 | - name: Run doc tests 66 | run: cargo test --workspace --all-features --doc 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | msrv: 1.76.0 11 | nightly: nightly-2024-07-07 12 | 13 | jobs: 14 | build-msrv: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install Rust 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ env.msrv }} 24 | 25 | - name: Cache cargo build 26 | uses: actions/cache@v4 27 | with: 28 | path: target 29 | key: ${{ runner.os }}-msrv-cargo-${{ hashFiles('Cargo.lock') }} 30 | restore-keys: ${{ runner.os }}-msrv-cargo 31 | 32 | - name: Build libraries 33 | run: cargo build -p externref -p externref-macro --all-features 34 | 35 | build: 36 | uses: ./.github/workflows/build-reusable.yml 37 | 38 | build-docker: 39 | needs: 40 | - build 41 | - build-msrv 42 | permissions: 43 | contents: read 44 | packages: write 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Cache Docker build 51 | uses: actions/cache@v4 52 | with: 53 | path: target/docker 54 | key: ${{ runner.os }}-docker-buildkit-${{ hashFiles('Cargo.lock') }} 55 | restore-keys: ${{ runner.os }}-docker-buildkit 56 | 57 | - name: Extract Docker metadata 58 | id: meta 59 | uses: docker/metadata-action@v4 60 | with: 61 | images: ghcr.io/${{ github.repository }} 62 | 63 | - name: Log in to Container registry 64 | uses: docker/login-action@v2 65 | with: 66 | registry: ghcr.io 67 | username: ${{ github.actor }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Set up Docker Buildx 71 | id: buildx 72 | uses: docker/setup-buildx-action@v2 73 | with: 74 | driver-opts: image=moby/buildkit:buildx-stable-1 75 | - name: Identify Buildx container 76 | run: | 77 | CONTAINER_ID=$(docker ps --filter=ancestor=moby/buildkit:buildx-stable-1 --format='{{ .ID }}') 78 | echo "buildx_container=$CONTAINER_ID" | tee -a "$GITHUB_ENV" 79 | 80 | - name: Restore cache 81 | run: | 82 | if [[ -f target/docker/cache.db ]]; then 83 | docker cp target/docker/. "$BUILDER:/var/lib/buildkit" 84 | docker restart "$BUILDER" 85 | # Wait until the container is restarted 86 | sleep 5 87 | fi 88 | docker buildx du # Check the restored cache 89 | env: 90 | BUILDER: ${{ env.buildx_container }} 91 | 92 | - name: Build image 93 | uses: docker/build-push-action@v3 94 | with: 95 | context: . 96 | file: crates/cli/Dockerfile 97 | load: true 98 | tags: ${{ steps.meta.outputs.tags }} 99 | labels: ${{ steps.meta.outputs.labels }} 100 | 101 | # We want to only store cache volumes (type=exec.cachemount) since 102 | # their creation is computationally bound as opposed to other I/O-bound volume types. 103 | - name: Extract image cache 104 | run: | 105 | docker buildx prune --force --filter=type=regular 106 | docker buildx prune --force --filter=type=source.local 107 | rm -rf target/docker && mkdir -p target/docker 108 | docker cp "$BUILDER:/var/lib/buildkit/." target/docker 109 | du -ah -d 1 target/docker 110 | env: 111 | BUILDER: ${{ env.buildx_container }} 112 | 113 | - name: Test image (--help) 114 | run: docker run --rm "$IMAGE_TAG" --help 115 | env: 116 | IMAGE_TAG: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} 117 | - name: Test image (transform) 118 | run: | 119 | docker run -i --rm --env RUST_LOG=externref=debug "$IMAGE_TAG" - \ 120 | < crates/cli/tests/test.wasm \ 121 | > /dev/null 122 | env: 123 | IMAGE_TAG: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} 124 | 125 | - name: Publish image 126 | if: github.event_name == 'push' 127 | run: docker push "$IMAGE_TAG" 128 | env: 129 | IMAGE_TAG: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} 130 | 131 | document: 132 | if: github.event_name == 'push' 133 | needs: 134 | - build 135 | - build-msrv 136 | permissions: 137 | contents: write 138 | runs-on: ubuntu-latest 139 | 140 | steps: 141 | - uses: actions/checkout@v4 142 | 143 | - name: Install Rust 144 | uses: dtolnay/rust-toolchain@master 145 | with: 146 | toolchain: ${{ env.nightly }} 147 | 148 | - name: Cache cargo build 149 | uses: actions/cache@v4 150 | with: 151 | path: target 152 | key: ${{ runner.os }}-document-cargo-${{ hashFiles('Cargo.lock') }} 153 | restore-keys: ${{ runner.os }}-document-cargo 154 | 155 | - name: Build docs 156 | run: | 157 | cargo clean --doc && cargo rustdoc -p externref --all-features -- --cfg docsrs \ 158 | && cargo rustdoc -p externref-macro -- --cfg docsrs 159 | 160 | - name: Deploy 161 | uses: JamesIves/github-pages-deploy-action@v4 162 | with: 163 | branch: gh-pages 164 | folder: target/doc 165 | single-commit: true 166 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ "v*" ] 6 | workflow_dispatch: {} 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | release: 14 | permissions: 15 | contents: write 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | target: x86_64-unknown-linux-gnu 21 | - os: macos-latest 22 | target: x86_64-apple-darwin 23 | - os: macos-latest 24 | target: aarch64-apple-darwin 25 | - os: windows-latest 26 | target: x86_64-pc-windows-msvc 27 | 28 | runs-on: ${{ matrix.os }} 29 | name: Release ${{ matrix.target }} 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Determine release type 35 | id: release-type 36 | run: | 37 | if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+[.][0-9]+[.][0-9]+$ ]]; then 38 | echo 'type=release' >> "$GITHUB_OUTPUT" 39 | else 40 | echo 'type=prerelease' >> "$GITHUB_OUTPUT" 41 | fi 42 | 43 | - name: Install Rust 44 | uses: dtolnay/rust-toolchain@master 45 | with: 46 | toolchain: stable 47 | targets: ${{ matrix.target }} 48 | 49 | - name: Cache cargo build 50 | uses: actions/cache@v4 51 | with: 52 | path: target 53 | key: ${{ runner.os }}-release-cargo-${{ hashFiles('Cargo.lock') }} 54 | restore-keys: ${{ runner.os }}-release-cargo 55 | 56 | - name: Build CLI app 57 | run: cargo build -p externref-cli --profile=executable --target=${{ matrix.target }} --all-features --locked 58 | - name: Package archive 59 | id: package 60 | run: ./crates/cli/package.sh ${REF#refs/*/} 61 | env: 62 | OS: ${{ matrix.os }} 63 | TARGET: ${{ matrix.target }} 64 | REF: ${{ github.ref }} 65 | - name: Publish archive 66 | uses: softprops/action-gh-release@v1 67 | if: github.event_name == 'push' 68 | with: 69 | draft: false 70 | files: ${{ steps.package.outputs.archive }} 71 | prerelease: ${{ steps.release-type.outputs.type == 'prerelease' }} 72 | - name: Attach archive to workflow 73 | uses: actions/upload-artifact@v3 74 | if: github.event_name == 'workflow_dispatch' 75 | with: 76 | name: externref-${{ matrix.target }} 77 | path: ${{ steps.package.outputs.archive }} 78 | -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | name: Scheduled checks 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 * * MON" 6 | 7 | jobs: 8 | build: 9 | uses: ./.github/workflows/build-reusable.yml 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target 3 | # IDE 4 | .idea 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ## 0.3.0-beta.1 - 2024-09-29 9 | 10 | ### Added 11 | 12 | - Support `no_std` compilation mode for the library. 13 | - Explain guard detection errors and provide more specific instructions on how to avoid them 14 | (e.g., use `debug = 1` in the profile config). 15 | 16 | ### Changed 17 | 18 | - Bump minimum supported Rust version to 1.76. 19 | 20 | ## 0.2.0 - 2023-06-03 21 | 22 | ### Added 23 | 24 | - Support upcasting and downcasting of `Resource`s. 25 | - Support expressions in `link_name` / `export_name` attributes, such as 26 | `#[export_name = concat("prefix_", stringify!($name))]` for use in macros (`$name` 27 | is a macro variable). Previously, only string literals were supported. 28 | - Support re-exporting the crate by adding an optional `crate` parameter 29 | to the `#[externref]` attribute, e.g. `#[externref(crate = "other_crate::_externref")]` 30 | where `other_crate` defines `pub use externref as _externref`. 31 | - **CLI:** add a command-line application for transforming WASM modules, and the Docker image 32 | with this application. 33 | 34 | ### Changed 35 | 36 | - **Macro:** update `syn` dependency to 2.0. 37 | - Bump minimum supported Rust version to 1.66. 38 | 39 | ### Fixed 40 | 41 | - Fix an incorrect conditional compilation attribute for a tracing event 42 | in the processor module. 43 | - Fix / document miscompilation resulting from optimization tools inlining 44 | an `externref`-operation function. The processor now returns an error 45 | if it encounters such an inlined function, and the docs mention how to avoid 46 | inlining (do not run WASM optimization tools before the `externref` processor). 47 | 48 | ## 0.1.0 - 2022-10-29 49 | 50 | The initial release of `externref`. 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `externref` 2 | 3 | This project welcomes contribution from everyone, which can take form of suggestions / feature requests, bug reports, or pull requests. 4 | This document provides guidance how best to contribute. 5 | 6 | ## Bug reports and feature requests 7 | 8 | For bugs or when asking for help, please use the bug issue template and include enough details so that your observations 9 | can be reproduced. 10 | 11 | For feature requests, please use the feature request issue template and describe the intended use case(s) and motivation 12 | to go for them. If possible, include your ideas how to implement the feature, potential alternatives and disadvantages. 13 | 14 | ## Pull requests 15 | 16 | Please use the pull request template when submitting a PR. List the major goal(s) achieved by the PR 17 | and describe the motivation behind it. If applicable, like to the related issue(s). 18 | 19 | Optimally, you should check locally that the CI checks pass before submitting the PR. Checks included in the CI 20 | include: 21 | 22 | - Formatting using `cargo fmt --all -- --config imports_granularity=Crate --config group_imports=StdExternalCrate` 23 | - Linting using `cargo clippy` 24 | - Linting the dependency graph using [`cargo deny`](https://crates.io/crates/cargo-deny) 25 | - Running the test suite using `cargo test` 26 | 27 | A complete list of checks can be viewed in [the CI workflow file](.github/workflows/ci.yml). The checks are run 28 | on the latest stable Rust version. 29 | 30 | ### MSRV checks 31 | 32 | A part of the CI assertions is the minimum supported Rust version (MSRV). If this check fails, consult the error messages. Depending on 33 | the error (e.g., whether it is caused by a newer language feature used in the PR code, or in a dependency), 34 | you might want to rework the PR, get rid of the offending dependency, or bump the MSRV; don't hesitate to consult the maintainers. 35 | 36 | ## Code of Conduct 37 | 38 | Be polite and respectful. 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/cli", "crates/lib", "crates/macro", "e2e-tests"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.3.0-beta.1" 7 | edition = "2021" 8 | rust-version = "1.76" 9 | authors = ["Alex Ostrovski "] 10 | license = "MIT OR Apache-2.0" 11 | repository = "https://github.com/slowli/externref" 12 | 13 | [workspace.dependencies] 14 | # General-purpose dependencies 15 | anyhow = "1.0.98" 16 | clap = { version = "4.5.36", features = ["derive", "wrap_help"] } 17 | dlmalloc = "0.2.8" 18 | once_cell = "1.21.3" 19 | predicates = { version = "3.1.3", default-features = false } 20 | proc-macro2 = "1.0" 21 | quote = "1.0" 22 | syn = "2.0" 23 | tracing = "0.1.41" 24 | tracing-subscriber = "0.3.19" 25 | walrus = "0.23.3" 26 | 27 | # Test dependencies 28 | assert_matches = "1.5.0" 29 | doc-comment = "0.3.3" 30 | term-transcript = { version = "=0.4.0-beta.1", features = ["portable-pty"] } 31 | test-casing = "0.1.3" 32 | tracing-capture = "0.1.0" 33 | trybuild = "1.0.104" 34 | version-sync = "0.9.4" 35 | wasmtime = "31.0.0" 36 | wat = "1.228.0" 37 | 38 | # Internal dependencies 39 | externref-macro = { version = "=0.3.0-beta.1", path = "crates/macro" } 40 | externref = { version = "=0.3.0-beta.1", path = "crates/lib", default-features = false } 41 | # ^ We require an exact version in order to simplify crate evolution (e.g., to not worry 42 | # that future internal changes in macro implementations will break previous versions 43 | # of the `externref` crate). 44 | 45 | # Profile for WASM modules generated in E2E tests 46 | [profile.wasm] 47 | inherits = "release" 48 | panic = "abort" 49 | codegen-units = 1 50 | opt-level = "z" # Optimize for size, rather than speed 51 | lto = true 52 | 53 | # Profile for workspace executables 54 | [profile.executable] 55 | inherits = "release" 56 | strip = true 57 | codegen-units = 1 58 | lto = true 59 | 60 | # Required to properly inline surrogate `externref`s (see "Limitations" in the crate docs) 61 | [profile.dev.package.externref-test] 62 | debug = 1 63 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2022-current Developers of externref 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Low-Cost Reference Type Shims For WASM Modules 2 | 3 | [![Build Status](https://github.com/slowli/externref/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/externref/actions) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/externref#license) 5 | 6 | A [reference type] (aka `externref` or `anyref`) is an opaque reference made available to 7 | a WASM module by the host environment. Such references cannot be forged in the WASM code 8 | and can be associated with arbitrary host data, thus making them a good alternative to 9 | ad-hoc handles (e.g., numeric ones). References cannot be stored in WASM linear memory; they are 10 | confined to the stack and tables with `externref` elements. 11 | 12 | Rust does not support reference types natively; there is no way to produce an import / export 13 | that has `externref` as an argument or a return type. [`wasm-bindgen`] patches WASM if 14 | `externref`s are enabled. This library strives to accomplish the same goal for generic 15 | low-level WASM ABIs (`wasm-bindgen` is specialized for browser hosts). 16 | 17 | ## Project overview 18 | 19 | The project consists of the following crates: 20 | 21 | - [`externref`](crates/lib): The library providing more typesafe `externref`s for Rust 22 | - [`externref-macro`](crates/macro): Procedural macro for the library 23 | - [`externref-cli`](crates/cli): CLI app for WASM transforms based on the library 24 | 25 | ## Project status 🚧 26 | 27 | Experimental; it may be the case that the processor produces invalid WASM 28 | in some corner cases (please report this as an issue if it does). 29 | 30 | ## Contributing 31 | 32 | All contributions are welcome! See [the contributing guide](CONTRIBUTING.md) to help 33 | you get involved. 34 | 35 | ## License 36 | 37 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 38 | or [MIT license](LICENSE-MIT) at your option. 39 | 40 | Unless you explicitly state otherwise, any contribution intentionally submitted 41 | for inclusion in `externref` by you, as defined in the Apache-2.0 license, 42 | shall be dual licensed as above, without any additional terms or conditions. 43 | 44 | [reference type]: https://webassembly.github.io/spec/core/syntax/types.html#reference-types 45 | [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen 46 | -------------------------------------------------------------------------------- /crates/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externref-cli" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | keywords = ["externref", "anyref", "wasm"] 11 | categories = ["command-line-utilities", "wasm", "development-tools::ffi"] 12 | description = "CLI for `externref` providing WASM module processing" 13 | 14 | [[bin]] 15 | name = "externref" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | anyhow.workspace = true 20 | clap.workspace = true 21 | tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } 22 | 23 | # Internal dependencies 24 | externref = { workspace = true, features = ["processor"] } 25 | 26 | [dev-dependencies] 27 | term-transcript.workspace = true 28 | test-casing.workspace = true 29 | 30 | [features] 31 | default = ["tracing"] 32 | # Enables tracing output during program execution. 33 | tracing = ["tracing-subscriber", "externref/tracing"] 34 | -------------------------------------------------------------------------------- /crates/cli/Dockerfile: -------------------------------------------------------------------------------- 1 | # Docker image for the `externref` CLI executable. 2 | # See the CLI crate readme for the usage instructions. 3 | 4 | FROM clux/muslrust:stable AS builder 5 | ADD ../.. ./ 6 | ARG FEATURES=tracing 7 | RUN --mount=type=cache,id=cargo-registry,target=/root/.cargo/registry \ 8 | --mount=type=cache,id=artifacts,target=/volume/target \ 9 | cargo build -p externref-cli --profile=executable \ 10 | --no-default-features --features=$FEATURES \ 11 | --target-dir /volume/target && \ 12 | # Move the resulting executable so it doesn't get unmounted together with the cache 13 | mv /volume/target/x86_64-unknown-linux-musl/executable/externref /volume/externref 14 | 15 | FROM gcr.io/distroless/static-debian11 16 | COPY --from=builder /volume/externref / 17 | ENTRYPOINT ["/externref"] 18 | -------------------------------------------------------------------------------- /crates/cli/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/cli/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /crates/cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI for `externref` Crate 2 | 3 | [![Build Status](https://github.com/slowli/externref/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/externref/actions) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/externref#license) 5 | 6 | This crate provides command-line interface for [`externref`]. It allows transforming 7 | WASM modules that use `externref` shims to use real `externref` types. 8 | 9 | ## Installation 10 | 11 | Install with 12 | 13 | ```shell 14 | cargo install --locked externref-cli 15 | # This will install `externref` executable, which can be checked 16 | # as follows: 17 | externref --help 18 | ``` 19 | 20 | By default, tracing is enabled via the `tracing` crate feature. You can disable 21 | the feature manually by adding a `--no-default-features` arg to the installation command. 22 | Tracing is performed with the `externref::*` targets, mostly on the `DEBUG` and `INFO` levels. 23 | Tracing events are output to the stderr using [the standard subscriber][fmt-subscriber]; 24 | its filtering can be configured using the `RUST_LOG` env variable 25 | (e.g., `RUST_LOG=externref=debug`). 26 | 27 | Alternatively, you may use the app Docker image [as described below](#using-docker-image), 28 | or download a pre-built app binary for popular targets (x86_64 for Linux / macOS / Windows 29 | and AArch64 for macOS) 30 | from [GitHub Releases](https://github.com/slowli/externref/releases). 31 | 32 | ### Minimum supported Rust version 33 | 34 | The crate supports the latest stable Rust version. It may support previous stable Rust versions, 35 | but this is not guaranteed. 36 | 37 | ## Usage 38 | 39 | The executable provides the same functionality as the WASM [`processor`] 40 | from the `externref` crate. See its docs and the output of `externref --help` 41 | for a detailed description of available options. 42 | 43 | > **Warning** 44 | > 45 | > The processor should run before WASM optimization tools such as 46 | > `wasm-opt` from binaryen. 47 | 48 | ### Using Docker image 49 | 50 | As a lower-cost alternative to the local installation, you may install and use the CLI app 51 | from the [GitHub Container registry](https://github.com/slowli/externref/pkgs/container/externref). 52 | To run the app in a Docker container, use a command like 53 | 54 | ```shell 55 | docker run -i --rm ghcr.io/slowli/externref:main - \ 56 | < module.wasm \ 57 | > processed-module.wasm 58 | ``` 59 | 60 | Here, `-` is the argument to the CLI app instructing to read the input module from the stdin. 61 | To output tracing information, set the `RUST_LOG` env variable in the container, 62 | e.g. using `docker run --env RUST_LOG=debug ...`. 63 | 64 | ### Examples 65 | 66 | The terminal capture below demonstrates transforming a test WASM module. 67 | The capture includes the tracing output, which was switched on 68 | by setting the `RUST_LOG` env variable. Tracing info includes each transformed function 69 | and some other information that could be useful for debugging. 70 | 71 | ![Output with tracing][output-with-tracing] 72 | 73 | ## License 74 | 75 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 76 | or [MIT license](LICENSE-MIT) at your option. 77 | 78 | Unless you explicitly state otherwise, any contribution intentionally submitted 79 | for inclusion in `externref` by you, as defined in the Apache-2.0 license, 80 | shall be dual licensed as above, without any additional terms or conditions. 81 | 82 | [`externref`]: https://crates.io/crates/externref 83 | [fmt-subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/index.html 84 | [`processor`]: https://slowli.github.io/externref/externref/processor/ 85 | [output-with-tracing]: https://github.com/slowli/externref/raw/HEAD/crates/cli/tests/snapshots/with-tracing.svg?sanitize=true 86 | -------------------------------------------------------------------------------- /crates/cli/package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Script to create an archive with the release contents (the `externref` executable 4 | # and the supporting docs). 5 | 6 | set -e 7 | 8 | VERSION=$1 9 | if [[ "$VERSION" == '' ]]; then 10 | echo "Error: release version is not specified" 11 | exit 1 12 | fi 13 | echo "Packaging externref $VERSION for $TARGET..." 14 | 15 | CLI_DIR=$(dirname "$0") 16 | RELEASE_DIR="$CLI_DIR/release" 17 | ROOT_DIR="$CLI_DIR/../.." 18 | EXECUTABLE="$ROOT_DIR/target/$TARGET/executable/externref" 19 | 20 | if [[ "$OS" == 'windows-latest' ]]; then 21 | EXECUTABLE="$EXECUTABLE.exe" 22 | fi 23 | if [[ ! -x $EXECUTABLE ]]; then 24 | echo "Error: executable $EXECUTABLE does not exist" 25 | exit 1 26 | fi 27 | 28 | rm -rf "$RELEASE_DIR" && mkdir "$RELEASE_DIR" 29 | echo "Copying release files to $RELEASE_DIR..." 30 | cp "$EXECUTABLE" \ 31 | "$CLI_DIR/README.md" \ 32 | "$CLI_DIR/CHANGELOG.md" \ 33 | "$CLI_DIR/LICENSE-APACHE" \ 34 | "$CLI_DIR/LICENSE-MIT" \ 35 | "$RELEASE_DIR" 36 | 37 | cd "$RELEASE_DIR" 38 | echo "Creating release archive..." 39 | case $OS in 40 | ubuntu-latest | macos-latest) 41 | ARCHIVE="externref-$VERSION-$TARGET.tar.gz" 42 | tar czf "$ARCHIVE" ./* 43 | ;; 44 | windows-latest) 45 | ARCHIVE="externref-$VERSION-$TARGET.zip" 46 | 7z a "$ARCHIVE" ./* 47 | ;; 48 | *) 49 | echo "Unknown target: $TARGET" 50 | exit 1 51 | esac 52 | ls -l "$ARCHIVE" 53 | 54 | if [[ "$GITHUB_OUTPUT" != '' ]]; then 55 | echo "Outputting path to archive as GitHub step output: $RELEASE_DIR/$ARCHIVE" 56 | echo "archive=$RELEASE_DIR/$ARCHIVE" >> "$GITHUB_OUTPUT" 57 | fi 58 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | //! CLI for the `externref` crate. 2 | 3 | // Linter settings. 4 | #![warn(missing_debug_implementations, bare_trait_objects)] 5 | #![warn(clippy::all, clippy::pedantic)] 6 | #![allow(clippy::must_use_candidate, clippy::module_name_repetitions)] 7 | 8 | use std::{ 9 | fs, 10 | io::{self, Read as _, Write as _}, 11 | path::PathBuf, 12 | str::FromStr, 13 | }; 14 | 15 | use anyhow::{anyhow, ensure, Context}; 16 | use clap::Parser; 17 | use externref::processor::Processor; 18 | 19 | #[derive(Debug, Clone)] 20 | struct ModuleAndName { 21 | module: String, 22 | name: String, 23 | } 24 | 25 | impl FromStr for ModuleAndName { 26 | type Err = anyhow::Error; 27 | 28 | fn from_str(s: &str) -> Result { 29 | let (module, name) = s 30 | .split_once("::") 31 | .ok_or_else(|| anyhow!("function must be specified in the `module::name` format"))?; 32 | 33 | ensure!(!module.is_empty(), "module cannot be empty"); 34 | ensure!(module.is_ascii(), "module must contain ASCII chars only"); 35 | ensure!(!name.is_empty(), "name cannot be empty"); 36 | ensure!(name.is_ascii(), "name must contain ASCII chars only"); 37 | Ok(Self { 38 | module: module.to_owned(), 39 | name: name.to_owned(), 40 | }) 41 | } 42 | } 43 | 44 | /// CLI for transforming WASM modules with `externref` shims produced with the help 45 | /// of the `externref` crate. 46 | #[derive(Debug, Parser)] 47 | struct Cli { 48 | /// Path to the input WASM module. 49 | /// If set to `-`, the module will be read from the standard input. 50 | input: PathBuf, 51 | /// Path to the output WASM module. If not specified, the module will be emitted 52 | /// to the standard output. 53 | #[arg(long, short = 'o')] 54 | output: Option, 55 | /// Name of the exported `externref`s table where refs obtained from the host 56 | /// are placed. 57 | #[arg(long = "table", default_value = "externrefs")] 58 | export_table: String, 59 | /// Function to notify the host about dropped `externref`s specified 60 | /// in the `module::name` format. 61 | /// 62 | /// This function will be added as an import with a signature `(externref) -> ()` 63 | /// and will be called immediately before dropping each reference. 64 | #[arg(long = "drop-fn")] 65 | drop_fn: Option, 66 | } 67 | 68 | impl Cli { 69 | #[cfg(feature = "tracing")] 70 | fn configure_tracing() { 71 | use tracing_subscriber::{filter::EnvFilter, FmtSubscriber}; 72 | 73 | FmtSubscriber::builder() 74 | .without_time() 75 | .with_env_filter(EnvFilter::from_default_env()) 76 | .with_writer(io::stderr) 77 | .init(); 78 | } 79 | 80 | fn run(&self) -> anyhow::Result<()> { 81 | #[cfg(feature = "tracing")] 82 | Self::configure_tracing(); 83 | 84 | let module = self.read_input_module().with_context(|| { 85 | format!( 86 | "failed reading input module from `{}`", 87 | self.input.to_string_lossy() 88 | ) 89 | })?; 90 | 91 | let mut processor = Processor::default(); 92 | processor.set_ref_table(self.export_table.as_str()); 93 | if let Some(drop_fn) = &self.drop_fn { 94 | processor.set_drop_fn(&drop_fn.module, &drop_fn.name); 95 | } 96 | let processed = processor 97 | .process_bytes(&module) 98 | .context("failed processing module")?; 99 | 100 | self.write_output_module(&processed).with_context(|| { 101 | if let Some(path) = &self.output { 102 | format!("failed writing module to file `{}`", path.to_string_lossy()) 103 | } else { 104 | "failed writing module to standard output".to_owned() 105 | } 106 | }) 107 | } 108 | 109 | fn read_input_module(&self) -> anyhow::Result> { 110 | let bytes = if self.input.as_os_str() == "-" { 111 | let mut buffer = Vec::with_capacity(1_024); 112 | io::stdin().read_to_end(&mut buffer)?; 113 | buffer 114 | } else { 115 | fs::read(&self.input)? 116 | }; 117 | Ok(bytes) 118 | } 119 | 120 | fn write_output_module(&self, bytes: &[u8]) -> anyhow::Result<()> { 121 | if let Some(path) = &self.output { 122 | fs::write(path, bytes)?; 123 | } else { 124 | io::stdout().lock().write_all(bytes)?; 125 | } 126 | Ok(()) 127 | } 128 | } 129 | 130 | fn main() -> anyhow::Result<()> { 131 | Cli::parse().run() 132 | } 133 | -------------------------------------------------------------------------------- /crates/cli/tests/integration.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for the externref CLI. 2 | 3 | #![cfg(unix)] // sh-specific user inputs 4 | 5 | use term_transcript::{ 6 | svg::{Template, TemplateOptions}, 7 | test::TestConfig, 8 | ExitStatus, PtyCommand, ShellOptions, 9 | }; 10 | #[cfg(feature = "tracing")] 11 | use test_casing::{decorate, decorators::Retry}; 12 | 13 | fn template() -> Template { 14 | Template::new(TemplateOptions { 15 | window_frame: true, 16 | ..TemplateOptions::default() 17 | }) 18 | } 19 | 20 | fn test_config() -> TestConfig { 21 | let shell_options = ShellOptions::new(PtyCommand::default()) 22 | .with_cargo_path() 23 | .with_current_dir(env!("CARGO_MANIFEST_DIR")) 24 | .with_status_check("echo $?", |output| { 25 | let response = output.to_plaintext().ok()?; 26 | response.trim().parse().ok().map(ExitStatus) 27 | }); 28 | TestConfig::new(shell_options).with_template(template()) 29 | } 30 | 31 | #[cfg(feature = "tracing")] 32 | #[test] 33 | #[decorate(Retry::times(3))] // sometimes, the captured output includes `>` from the input 34 | fn cli_with_tracing() { 35 | // The WASM module is taken from the end-to-end test. We check it into the version control 36 | // in order for this test to be autonomous. 37 | test_config().test( 38 | "tests/snapshots/with-tracing.svg", 39 | ["RUST_LOG=externref=info \\\n \ 40 | externref --drop-fn test::drop -o /dev/null tests/test.wasm"], 41 | ); 42 | } 43 | 44 | /// This and the following tests ensure that the error message is human-readable. 45 | #[test] 46 | fn error_processing_module() { 47 | test_config().test( 48 | "tests/snapshots/error-processing.svg", 49 | ["externref --drop-fn test::drop -o /dev/null tests/integration.rs"], 50 | ); 51 | } 52 | 53 | #[test] 54 | fn error_specifying_drop_fn() { 55 | test_config().test( 56 | "tests/snapshots/error-drop-fn.svg", 57 | ["externref --drop-fn test_drop -o /dev/null tests/test.wasm"], 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /crates/cli/tests/snapshots/error-drop-fn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 |
$ externref --drop-fn test_drop -o /dev/null tests/test.wasm
83 |
error: invalid value 'test_drop' for '--drop-fn <DROP_FN>': function must be spe
cified in the `module::name` format 84 | 85 | For more information, try '--help'.
86 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /crates/cli/tests/snapshots/error-processing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
$ externref --drop-fn test::drop -o /dev/null tests/integration.rs
74 |
Error: failed processing module
 75 | 
 76 | Caused by:
 77 |     0: failed reading WASM module: magic header not detected: bad magic number -
expected=[ 78 | 0x0, 79 | 0x61, 80 | 0x73, 81 | 0x6d, 82 | ] actual=[ 83 | 0x2f, 84 | 0x2f, 85 | 0x21, 86 | 0x20, 87 | ] (at offset 0x0) 88 | 1: magic header not detected: bad magic number - expected=[ 89 | 0x0, 90 | 0x61, 91 | 0x73, 92 | 0x6d, 93 | ] actual=[ 94 | 0x2f, 95 | 0x2f, 96 | 0x21, 97 | 0x20, 98 | ] (at offset 0x0)
99 |
100 |
101 |
102 |
103 | 104 | HTML embedding not supported. 105 | Consult term-transcript docs for details. 106 | 107 |
108 |
109 | -------------------------------------------------------------------------------- /crates/cli/tests/snapshots/with-tracing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
$ RUST_LOG=externref=info \
68 |   externref --drop-fn test::drop -o /dev/null tests/test.wasm
69 |
 INFO process: externref::processor: parsed custom section functions.len=5
70 |  INFO process:replace_functions: externref::processor::state: replaced calls to 
externref imports replaced_count=13 71 | INFO process:process_functions:transform_import{module="test" name="send_messag
e"}: externref::processor::state: replaced function signature params=[I32, I32,
I32] results=[I32] new_params=[Ref(Externref), I32, I32] new_results=[Ref(Extern
ref)] 72 | INFO process:process_functions:transform_import{module="test" name="message_len
"}: externref::processor::state: replaced function signature params=[I32] result
s
=[I32] new_params=[Ref(Externref)] new_results=[I32] 73 | INFO process:process_functions:transform_export{name="test_export"}: externref:
:processor::state
: replaced function signature params=[I32] results=[] new_param
s
=[Ref(Externref)] new_results=[] 74 | INFO process:process_functions:transform_export{name="test_export_with_casts"}:
externref::processor::state: replaced function signature params=[I32] results=[
] new_params=[Ref(Externref)] new_results=[] 75 | INFO process:process_functions:transform_export{name="test_nulls"}: externref::
processor::state
: replaced function signature params=[I32] results=[] new_params
=
[Ref(Externref)] new_results=[]
76 |
77 |
78 |
79 |
80 | 81 | HTML embedding not supported. 82 | Consult term-transcript docs for details. 83 | 84 |
85 |
86 | -------------------------------------------------------------------------------- /crates/cli/tests/test.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowli/externref/52493cb14b751ae92f460db96bc735d05c09b140/crates/cli/tests/test.wasm -------------------------------------------------------------------------------- /crates/lib/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /crates/lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externref" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | keywords = ["externref", "anyref", "wasm"] 11 | categories = ["wasm", "development-tools::ffi", "no-std"] 12 | description = "Low-cost reference type shims for WASM modules" 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | # Set `docsrs` to enable unstable `doc(cfg(...))` attributes. 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [badges] 20 | maintenance = { status = "experimental" } 21 | 22 | [dependencies] 23 | externref-macro = { workspace = true, optional = true } 24 | # Processor dependencies 25 | anyhow = { workspace = true, optional = true } 26 | walrus = { workspace = true, optional = true } 27 | # Enables tracing during module processing 28 | tracing = { workspace = true, optional = true } 29 | 30 | [dev-dependencies] 31 | assert_matches.workspace = true 32 | doc-comment.workspace = true 33 | version-sync.workspace = true 34 | wat.workspace = true 35 | 36 | [features] 37 | default = ["macro"] 38 | # Enables `std`-specific features, like `Error` implementations 39 | std = [] 40 | # Re-exports the `externref` macro 41 | macro = ["externref-macro"] 42 | # Enables WASM module processing logic (the `processor` module) 43 | processor = ["std", "anyhow", "walrus"] 44 | 45 | [[test]] 46 | name = "processor" 47 | path = "tests/processor.rs" 48 | required-features = ["processor"] 49 | -------------------------------------------------------------------------------- /crates/lib/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/lib/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /crates/lib/README.md: -------------------------------------------------------------------------------- 1 | # Low-Cost Reference Type Shims For WASM Modules 2 | 3 | [![Build Status](https://github.com/slowli/externref/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/externref/actions) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/externref#license) 5 | ![rust 1.76+ required](https://img.shields.io/badge/rust-1.76+-blue.svg?label=Required%20Rust) 6 | ![no_std supported](https://img.shields.io/badge/no__std-tested-green.svg) 7 | 8 | **Documentation:** [![Docs.rs](https://docs.rs/externref/badge.svg)](https://docs.rs/externref/) 9 | [![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://slowli.github.io/externref/externref/) 10 | 11 | A [reference type] (aka `externref` or `anyref`) is an opaque reference made available to 12 | a WASM module by the host environment. Such references cannot be forged in the WASM code 13 | and can be associated with arbitrary host data, thus making them a good alternative to 14 | ad-hoc handles (e.g., numeric ones). References cannot be stored in WASM linear memory; they are 15 | confined to the stack and tables with `externref` elements. 16 | 17 | Rust does not support reference types natively; there is no way to produce an import / export 18 | that has `externref` as an argument or a return type. [`wasm-bindgen`] patches WASM if 19 | `externref`s are enabled. This library strives to accomplish the same goal for generic 20 | low-level WASM ABIs (`wasm-bindgen` is specialized for browser hosts). 21 | 22 | ## `externref` use cases 23 | 24 | Since `externref`s are completely opaque from the module perspective, the only way to use 25 | them is to send an `externref` back to the host as an argument of an imported function. 26 | (Depending on the function semantics, the call may or may not consume the `externref` 27 | and may or may not modify the underlying data; this is not reflected 28 | by the WASM function signature.) An `externref` cannot be dereferenced by the module, 29 | thus, the module cannot directly access or modify the data behind the reference. Indeed, 30 | the module cannot even be sure which kind of data is being referenced. 31 | 32 | It may seem that this limits `externref` utility significantly, 33 | but `externref`s can still be useful, e.g. to model [capability-based security] tokens 34 | or resource handles in the host environment. Another potential use case is encapsulating 35 | complex data that would be impractical to transfer across the WASM API boundary 36 | (especially if the data shape may evolve over time), and/or if interactions with data 37 | must be restricted from the module side. 38 | 39 | ## Usage 40 | 41 | Add this to your `Crate.toml`: 42 | 43 | ```toml 44 | [dependencies] 45 | externref = "0.3.0-beta.1" 46 | ``` 47 | 48 | 1. Use `Resource`s as arguments / return results for imported and/or exported functions 49 | in a WASM module in place of `externref`s. Reference args (including mutable references) 50 | and the `Option<_>` wrapper are supported as well. 51 | 2. Add the `#[externref]` proc macro on the imported / exported functions. 52 | 3. Transform the generated WASM module with the module processor 53 | from the corresponding module of the crate. 54 | 55 | As an alternative for the final step, there is a [CLI app](../cli) 56 | that can process WASM modules with slightly less fine-grained control. 57 | 58 | > **Important.** The processor should run before WASM optimization tools such as 59 | > `wasm-opt` from binaryen. 60 | 61 | ### Limitations 62 | 63 | If you compile WASM without compilation optimizations, you might get "incorrectly placed externref guard" errors during WASM processing. 64 | Currently, the only workaround is to switch off some debug info for the compiled WASM module, e.g. using a workspace manifest: 65 | 66 | ```toml,no_sync 67 | [profile.dev.package.your-wasm-module] 68 | debug = 1 # or "limited" if you're targeting MSRV 1.71+ 69 | ``` 70 | 71 | These errors shouldn't occur if WASM is compiled in the release mode. 72 | 73 | ### Examples 74 | 75 | Using the `#[externref]` macro and `Resource`s in WASM-targeting code: 76 | 77 | ```rust 78 | use externref::{externref, Resource}; 79 | 80 | // Two marker types for different resources. 81 | pub struct Arena(()); 82 | pub struct Bytes(()); 83 | 84 | #[cfg(target_arch = "wasm32")] 85 | #[externref] 86 | #[link(wasm_import_module = "arena")] 87 | extern "C" { 88 | // This import will have signature `(externref, i32) -> externref` 89 | // on host. 90 | fn alloc(arena: &Resource, size: usize) 91 | -> Option>; 92 | } 93 | 94 | // Fallback for non-WASM targets. 95 | #[cfg(not(target_arch = "wasm32"))] 96 | unsafe fn alloc(_: &Resource, _: usize) 97 | -> Option> { None } 98 | 99 | // This export will have signature `(externref) -> ()` on host. 100 | #[externref] 101 | #[export_name = "test_export"] 102 | pub extern "C" fn test_export(arena: &Resource) { 103 | let bytes = unsafe { alloc(arena, 42) }.expect("cannot allocate"); 104 | // Do something with `bytes`... 105 | } 106 | ``` 107 | 108 | See crate docs for more examples of usage and implementation details. 109 | 110 | ## Project status 🚧 111 | 112 | Experimental; it may be the case that the processor produces invalid WASM 113 | in some corner cases (please report this as an issue if it does). 114 | 115 | ## License 116 | 117 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 118 | or [MIT license](LICENSE-MIT) at your option. 119 | 120 | Unless you explicitly state otherwise, any contribution intentionally submitted 121 | for inclusion in `externref` by you, as defined in the Apache-2.0 license, 122 | shall be dual licensed as above, without any additional terms or conditions. 123 | 124 | [reference type]: https://webassembly.github.io/spec/core/syntax/types.html#reference-types 125 | [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen 126 | [capability-based security]: https://en.wikipedia.org/wiki/Capability-based_security 127 | -------------------------------------------------------------------------------- /crates/lib/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Errors produced by crate logic. 2 | 3 | use core::{fmt, str::Utf8Error}; 4 | 5 | use crate::alloc::String; 6 | 7 | /// Kind of a [`ReadError`]. 8 | #[derive(Debug)] 9 | #[non_exhaustive] 10 | pub enum ReadErrorKind { 11 | /// Unexpected end of the input. 12 | UnexpectedEof, 13 | /// Error parsing 14 | Utf8(Utf8Error), 15 | } 16 | 17 | impl fmt::Display for ReadErrorKind { 18 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | match self { 20 | Self::UnexpectedEof => formatter.write_str("reached end of input"), 21 | Self::Utf8(err) => write!(formatter, "{err}"), 22 | } 23 | } 24 | } 25 | 26 | impl ReadErrorKind { 27 | pub(crate) fn with_context(self, context: impl Into) -> ReadError { 28 | ReadError { 29 | kind: self, 30 | context: context.into(), 31 | } 32 | } 33 | } 34 | 35 | /// Errors that can occur when reading declarations of functions manipulating [`Resource`]s 36 | /// from a WASM module. 37 | /// 38 | /// [`Resource`]: crate::Resource 39 | #[derive(Debug)] 40 | pub struct ReadError { 41 | kind: ReadErrorKind, 42 | context: String, 43 | } 44 | 45 | impl fmt::Display for ReadError { 46 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | write!(formatter, "failed reading {}: {}", self.context, self.kind) 48 | } 49 | } 50 | 51 | #[cfg(feature = "std")] 52 | impl std::error::Error for ReadError { 53 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 54 | match &self.kind { 55 | ReadErrorKind::Utf8(err) => Some(err), 56 | ReadErrorKind::UnexpectedEof => None, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Low-cost [reference type] shims for WASM modules. 2 | //! 3 | //! Reference type (aka `externref` or `anyref`) is an opaque reference made available to 4 | //! a WASM module by the host environment. Such references cannot be forged in the WASM code 5 | //! and can be associated with arbitrary host data, thus making them a good alternative to 6 | //! ad-hoc handles (e.g., numeric ones). References cannot be stored in WASM linear memory; 7 | //! they are confined to the stack and tables with `externref` elements. 8 | //! 9 | //! Rust does not support reference types natively; there is no way to produce an import / export 10 | //! that has `externref` as an argument or a return type. [`wasm-bindgen`] patches WASM if 11 | //! `externref`s are enabled. This library strives to accomplish the same goal for generic 12 | //! low-level WASM ABIs (`wasm-bindgen` is specialized for browser hosts). 13 | //! 14 | //! # `externref` use cases 15 | //! 16 | //! Since `externref`s are completely opaque from the module perspective, the only way to use 17 | //! them is to send an `externref` back to the host as an argument of an imported function. 18 | //! (Depending on the function semantics, the call may or may not consume the `externref` 19 | //! and may or may not modify the underlying data; this is not reflected 20 | //! by the WASM function signature.) An `externref` cannot be dereferenced by the module, 21 | //! thus, the module cannot directly access or modify the data behind the reference. Indeed, 22 | //! the module cannot even be sure which kind of data is being referenced. 23 | //! 24 | //! It may seem that this limits `externref` utility significantly, 25 | //! but `externref`s can still be useful, e.g. to model [capability-based security] tokens 26 | //! or resource handles in the host environment. Another potential use case is encapsulating 27 | //! complex data that would be impractical to transfer across the WASM API boundary 28 | //! (especially if the data shape may evolve over time), and/or if interactions with data 29 | //! must be restricted from the module side. 30 | //! 31 | //! [capability-based security]: https://en.wikipedia.org/wiki/Capability-based_security 32 | //! 33 | //! # Usage 34 | //! 35 | //! 1. Use [`Resource`]s as arguments / return results for imported and/or exported functions 36 | //! in a WASM module in place of `externref`s . Reference args (including mutable references) 37 | //! and the `Option<_>` wrapper are supported as well. 38 | //! 2. Add the `#[externref]` proc macro on the imported / exported functions. 39 | //! 3. Post-process the generated WASM module with the [`processor`]. 40 | //! 41 | //! `Resource`s support primitive downcasting and upcasting with `Resource<()>` signalling 42 | //! a generic resource. Downcasting is *unchecked*; it is up to the `Resource` users to 43 | //! define a way to check the resource kind dynamically if necessary. One possible approach 44 | //! for this is defining a WASM import `fn(&Resource<()>) -> Kind`, where `Kind` is the encoded 45 | //! kind of the supplied resource, such as `i32`. 46 | //! 47 | //! # How it works 48 | //! 49 | //! The [`externref` macro](macro@externref) detects `Resource` args / return types 50 | //! for imported and exported functions. All `Resource` args or return types are replaced 51 | //! with `usize`s and a wrapper function is added that performs the necessary transform 52 | //! from / to `usize`. 53 | //! Additionally, a function signature describing where `Resource` args are located 54 | //! is recorded in a WASM custom section. 55 | //! 56 | //! To handle `usize` (~`i32` in WASM) <-> `externref` conversions, managing resources is performed 57 | //! using 3 function imports from a surrogate module: 58 | //! 59 | //! - Creating a `Resource` ("real" signature `fn(externref) -> usize`) stores a reference 60 | //! into an `externref` table and returns the table index. The index is what is actually 61 | //! stored within the `Resource`, meaning that `Resource`s can be easily placed on heap. 62 | //! - Getting a reference from a `Resource` ("real" signature `fn(usize) -> externref`) 63 | //! is an indexing operation for the `externref` table. 64 | //! - [`Resource::drop()`] ("real" signature `fn(usize)`) removes the reference from the table. 65 | //! 66 | //! Real `externref`s are patched back to the imported / exported functions 67 | //! by the WASM module post-processor: 68 | //! 69 | //! - Imports from a surrogate module referenced by `Resource` methods are replaced 70 | //! with local WASM functions. Functions for getting an `externref` from the table 71 | //! and dropping an `externref` are more or less trivial. Storing an `externref` is less so; 72 | //! we don't want to excessively grow the `externref`s table, thus we search for null refs 73 | //! among its existing elements first, and only grow the table if all existing table elements are 74 | //! occupied. 75 | //! - Patching changes function types, and as a result types of some locals. 76 | //! This is OK because the post-processor also changes the signatures of affected 77 | //! imported / exported functions. The success relies on the fact that 78 | //! a reference is only stored *immediately* after receiving it from the host; 79 | //! likewise, a reference is only obtained *immediately* before passing it to the host. 80 | //! `Resource`s can be dropped anywhere, but the corresponding `externref` removal function 81 | //! does not need its type changed. 82 | //! 83 | //! [reference type]: https://webassembly.github.io/spec/core/syntax/types.html#reference-types 84 | //! [`wasm-bindgen`]: https://crates.io/crates/wasm-bindgen 85 | //! 86 | //! ## Limitations 87 | //! 88 | //! With debug info enabled, surrogate `usize`s may be spilled onto the shadow stack (part 89 | //! of the WASM linear memory used by `rustc` / LLVM when the main WASM stack is insufficient). 90 | //! This makes replacing these surrogates with `externref`s much harder (essentially, it'd require symbolic execution 91 | //! of the affected function to find out which locals should be replaced with `externref`s). This behavior should be detected 92 | //! by the [processor](processor), leading to "incorrectly placed externref guard" errors. Currently, 93 | //! the only workaround is to [set debug info level](https://doc.rust-lang.org/cargo/reference/profiles.html#debug) 94 | //! to `limited` or below for the compiled WASM module. 95 | //! 96 | //! # Crate features 97 | //! 98 | //! ## `std` 99 | //! 100 | //! *(Off by default)* 101 | //! 102 | //! Enables `std`-specific features, like [`Error`](std::error::Error) implementations for error types. 103 | //! 104 | //! ## `processor` 105 | //! 106 | //! *(Off by default)* 107 | //! 108 | //! Enables WASM module processing via the [`processor`] module. Requires the `std` feature. 109 | //! 110 | //! ## `tracing` 111 | //! 112 | //! *(Off by default)* 113 | //! 114 | //! Enables tracing during [module processing](processor) with the [`tracing`] facade. 115 | //! Tracing events / spans mostly use `INFO` and `DEBUG` levels. 116 | //! 117 | //! [`tracing`]: https://docs.rs/tracing/ 118 | //! 119 | //! # Examples 120 | //! 121 | //! Using the `#[externref]` macro and `Resource`s in WASM-targeting code: 122 | //! 123 | //! ```no_run 124 | //! use externref::{externref, Resource}; 125 | //! 126 | //! // Two marker types for different resources. 127 | //! pub struct Sender(()); 128 | //! pub struct Bytes(()); 129 | //! 130 | //! #[externref] 131 | //! #[link(wasm_import_module = "test")] 132 | //! extern "C" { 133 | //! // This import will have signature `(externref, i32, i32) -> externref` 134 | //! // on host. 135 | //! fn send_message( 136 | //! sender: &Resource, 137 | //! message_ptr: *const u8, 138 | //! message_len: usize, 139 | //! ) -> Resource; 140 | //! 141 | //! // `Option`s are used to deal with null references. This function will have 142 | //! // `(externref) -> i32` signature. 143 | //! fn message_len(bytes: Option<&Resource>) -> usize; 144 | //! // This one has `() -> externref` signature. 145 | //! fn last_sender() -> Option>; 146 | //! } 147 | //! 148 | //! // This export will have signature `(externref)` on host. 149 | //! #[externref] 150 | //! #[export_name = "test_export"] 151 | //! pub extern "C" fn test_export(sender: Resource) { 152 | //! let messages: Vec<_> = ["test", "42", "some other string"] 153 | //! .into_iter() 154 | //! .map(|msg| { 155 | //! unsafe { send_message(&sender, msg.as_ptr(), msg.len()) } 156 | //! }) 157 | //! .collect(); 158 | //! // ... 159 | //! // All 4 resources are dropped when exiting the function. 160 | //! } 161 | //! ``` 162 | 163 | #![cfg_attr(not(feature = "std"), no_std)] 164 | // Documentation settings. 165 | #![cfg_attr(docsrs, feature(doc_cfg))] 166 | #![doc(html_root_url = "https://docs.rs/externref/0.3.0-beta.1")] 167 | // Linter settings. 168 | #![warn(missing_debug_implementations, missing_docs, bare_trait_objects)] 169 | #![warn(clippy::all, clippy::pedantic)] 170 | #![allow( 171 | clippy::must_use_candidate, 172 | clippy::module_name_repetitions, 173 | clippy::inline_always 174 | )] 175 | 176 | use core::{alloc::Layout, fmt, marker::PhantomData, mem, ptr}; 177 | 178 | #[cfg(feature = "macro")] 179 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 180 | pub use externref_macro::externref; 181 | 182 | pub use crate::{ 183 | error::{ReadError, ReadErrorKind}, 184 | signature::{BitSlice, BitSliceBuilder, Function, FunctionKind}, 185 | }; 186 | 187 | mod error; 188 | #[cfg(feature = "processor")] 189 | #[cfg_attr(docsrs, doc(cfg(feature = "processor")))] 190 | pub mod processor; 191 | mod signature; 192 | 193 | // Polyfill for `alloc` types. 194 | mod alloc { 195 | #[cfg(not(feature = "std"))] 196 | extern crate alloc as std; 197 | 198 | pub(crate) use std::{format, string::String}; 199 | } 200 | 201 | /// `externref` surrogate. 202 | /// 203 | /// The post-processing logic replaces variables of this type with real `externref`s. 204 | #[doc(hidden)] // should only be used by macro-generated code 205 | #[repr(transparent)] 206 | pub struct ExternRef(usize); 207 | 208 | impl fmt::Debug for ExternRef { 209 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 210 | formatter.debug_struct("ExternRef").finish_non_exhaustive() 211 | } 212 | } 213 | 214 | impl ExternRef { 215 | /// Guard for imported function wrappers. The processor checks that each transformed function 216 | /// has this guard as the first instruction. 217 | /// 218 | /// # Safety 219 | /// 220 | /// This guard should only be inserted by the `externref` macro. 221 | #[inline(always)] 222 | pub unsafe fn guard() { 223 | #[cfg(target_arch = "wasm32")] 224 | #[link(wasm_import_module = "externref")] 225 | extern "C" { 226 | #[link_name = "guard"] 227 | fn guard(); 228 | } 229 | 230 | #[cfg(target_arch = "wasm32")] 231 | guard(); 232 | } 233 | } 234 | 235 | #[cfg(target_arch = "wasm32")] 236 | #[link(wasm_import_module = "externref")] 237 | extern "C" { 238 | #[link_name = "get"] 239 | fn get_externref(id: usize) -> ExternRef; 240 | } 241 | 242 | #[cfg(not(target_arch = "wasm32"))] 243 | unsafe fn get_externref(id: usize) -> ExternRef { 244 | ExternRef(id) 245 | } 246 | 247 | #[cfg(target_arch = "wasm32")] 248 | #[link(wasm_import_module = "externref")] 249 | extern "C" { 250 | #[link_name = "insert"] 251 | fn insert_externref(id: ExternRef) -> usize; 252 | } 253 | 254 | #[cfg(not(target_arch = "wasm32"))] 255 | #[allow(clippy::needless_pass_by_value)] 256 | unsafe fn insert_externref(id: ExternRef) -> usize { 257 | id.0 258 | } 259 | 260 | /// Host resource exposed to WASM. 261 | /// 262 | /// Internally, a resource is just an index into the `externref`s table; thus, it is completely 263 | /// valid to store `Resource`s on heap (in a `Vec`, thread-local storage, etc.). The type param 264 | /// can be used for type safety. 265 | #[derive(Debug)] 266 | #[repr(C)] 267 | pub struct Resource { 268 | id: usize, 269 | _ty: PhantomData, 270 | } 271 | 272 | impl Resource { 273 | /// Creates a new resource converting it from. 274 | /// 275 | /// # Safety 276 | /// 277 | /// This method must be called with an `externref` obtained from the host (as a return 278 | /// type for an imported function or an argument for an exported function); it is not 279 | /// a "real" `usize`. The proper use is ensured by the [`externref`] macro. 280 | #[doc(hidden)] // should only be used by macro-generated code 281 | #[inline(always)] 282 | pub unsafe fn new(id: ExternRef) -> Option { 283 | let id = insert_externref(id); 284 | if id == usize::MAX { 285 | None 286 | } else { 287 | Some(Self { 288 | id, 289 | _ty: PhantomData, 290 | }) 291 | } 292 | } 293 | 294 | #[doc(hidden)] // should only be used by macro-generated code 295 | #[inline(always)] 296 | pub unsafe fn new_non_null(id: ExternRef) -> Self { 297 | let id = insert_externref(id); 298 | assert!( 299 | id != usize::MAX, 300 | "Passed null `externref` as non-nullable arg" 301 | ); 302 | Self { 303 | id, 304 | _ty: PhantomData, 305 | } 306 | } 307 | 308 | /// Obtains an `externref` from this resource. 309 | /// 310 | /// # Safety 311 | /// 312 | /// The returned value of this method must be passed as an `externref` to the host 313 | /// (as a return type of an exported function or an argument of the imported function); 314 | /// it is not a "real" `usize`. The proper use is ensured by the [`externref`] macro. 315 | #[doc(hidden)] // should only be used by macro-generated code 316 | #[inline(always)] 317 | pub unsafe fn raw(this: Option<&Self>) -> ExternRef { 318 | get_externref(match this { 319 | None => usize::MAX, 320 | Some(resource) => resource.id, 321 | }) 322 | } 323 | 324 | /// Obtains an `externref` from this resource and drops the resource. 325 | #[doc(hidden)] // should only be used by macro-generated code 326 | #[inline(always)] 327 | #[allow(clippy::needless_pass_by_value)] 328 | pub unsafe fn take_raw(this: Option) -> ExternRef { 329 | get_externref(match this { 330 | None => usize::MAX, 331 | Some(resource) => resource.id, 332 | }) 333 | } 334 | 335 | /// Upcasts this resource to a generic resource. 336 | pub fn upcast(self) -> Resource<()> { 337 | Resource { 338 | id: self.leak_id(), 339 | _ty: PhantomData, 340 | } 341 | } 342 | 343 | #[inline] 344 | fn leak_id(self) -> usize { 345 | let id = self.id; 346 | mem::forget(self); 347 | id 348 | } 349 | 350 | /// Upcasts a reference to this resource to a generic resource reference. 351 | pub fn upcast_ref(&self) -> &Resource<()> { 352 | debug_assert_eq!(Layout::new::(), Layout::new::>()); 353 | 354 | let ptr = ptr::from_ref(self).cast::>(); 355 | unsafe { 356 | // SAFETY: All resource types have identical alignment (thanks to `repr(C)`), 357 | // hence, casting among them is safe. 358 | &*ptr 359 | } 360 | } 361 | } 362 | 363 | impl Resource<()> { 364 | /// Downcasts this generic resource to a specific type. 365 | /// 366 | /// # Safety 367 | /// 368 | /// No checks are performed that the resource actually encapsulates what is meant 369 | /// by `Resource`. It is up to the caller to check this beforehand (e.g., by calling 370 | /// a WASM import taking `&Resource<()>` and returning an app-specific resource kind). 371 | pub unsafe fn downcast_unchecked(self) -> Resource { 372 | Resource { 373 | id: self.leak_id(), 374 | _ty: PhantomData, 375 | } 376 | } 377 | } 378 | 379 | /// Drops the `externref` associated with this resource. 380 | impl Drop for Resource { 381 | #[inline(always)] 382 | fn drop(&mut self) { 383 | #[cfg(target_arch = "wasm32")] 384 | #[link(wasm_import_module = "externref")] 385 | extern "C" { 386 | #[link_name = "drop"] 387 | fn drop_externref(id: usize); 388 | } 389 | 390 | #[cfg(not(target_arch = "wasm32"))] 391 | unsafe fn drop_externref(_id: usize) { 392 | // Do nothing 393 | } 394 | 395 | unsafe { drop_externref(self.id) }; 396 | } 397 | } 398 | 399 | #[cfg(doctest)] 400 | doc_comment::doctest!("../README.md"); 401 | -------------------------------------------------------------------------------- /crates/lib/src/processor/error.rs: -------------------------------------------------------------------------------- 1 | //! Processing errors. 2 | 3 | use std::{error, fmt}; 4 | 5 | use crate::ReadError; 6 | 7 | /// Location of a `Resource`: a function argument or a return type. 8 | #[derive(Debug)] 9 | pub enum Location { 10 | /// Argument with the specified zero-based index. 11 | Arg(usize), 12 | /// Return type with the specified zero-based index. 13 | ReturnType(usize), 14 | } 15 | 16 | impl fmt::Display for Location { 17 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | match self { 19 | Self::Arg(idx) => write!(formatter, "arg #{idx}"), 20 | Self::ReturnType(idx) => write!(formatter, "return type #{idx}"), 21 | } 22 | } 23 | } 24 | 25 | /// Errors that can occur when [processing] a WASM module. 26 | /// 27 | /// [processing]: super::Processor::process() 28 | #[derive(Debug)] 29 | #[non_exhaustive] 30 | pub enum Error { 31 | /// Error reading the custom section with function declarations from the module. 32 | Read(ReadError), 33 | /// Error parsing the WASM module. 34 | Wasm(anyhow::Error), 35 | 36 | /// Unexpected type of an import (expected a function). 37 | UnexpectedImportType { 38 | /// Name of the module. 39 | module: String, 40 | /// Name of the function. 41 | name: String, 42 | }, 43 | /// Missing exported function with the enclosed name. 44 | NoExport(String), 45 | /// Unexpected type of an export (expected a function). 46 | UnexpectedExportType(String), 47 | /// Imported or exported function has unexpected arity. 48 | UnexpectedArity { 49 | /// Name of the module; `None` for exported functions. 50 | module: Option, 51 | /// Name of the function. 52 | name: String, 53 | /// Expected arity of the function. 54 | expected_arity: usize, 55 | /// Actual arity of the function. 56 | real_arity: usize, 57 | }, 58 | /// Argument or return type of a function has unexpected type. 59 | UnexpectedType { 60 | /// Name of the module; `None` for exported functions. 61 | module: Option, 62 | /// Name of the function. 63 | name: String, 64 | /// Location of an argument / return type in the function. 65 | location: Location, 66 | /// Actual type of the function (the expected type is always `i32`). 67 | real_type: walrus::ValType, 68 | }, 69 | 70 | /// Incorrectly placed `externref` guard. This is caused by processing the WASM module 71 | /// with external tools (e.g., `wasm-opt`) before using this processor. 72 | IncorrectGuard { 73 | /// Name of the function with an incorrectly placed guard. 74 | function_name: Option, 75 | /// WASM bytecode offset of the offending guard. 76 | code_offset: Option, 77 | }, 78 | /// Unexpected call to a function returning `externref`. Such calls should be confined 79 | /// in order for the processor to work properly. Like with [`Self::IncorrectGuard`], 80 | /// such errors should only be caused by external tools (e.g., `wasm-opt`). 81 | UnexpectedCall { 82 | /// Name of the function containing an unexpected call. 83 | function_name: Option, 84 | /// WASM bytecode offset of the offending call. 85 | code_offset: Option, 86 | }, 87 | } 88 | 89 | impl fmt::Display for Error { 90 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | const EXTERNAL_TOOL_TIP: &str = "This can be caused by an external WASM manipulation tool \ 92 | such as `wasm-opt`. Please run such tools *after* the externref processor."; 93 | 94 | match self { 95 | Self::Read(err) => write!(formatter, "failed reading WASM custom section: {err}"), 96 | Self::Wasm(err) => write!(formatter, "failed reading WASM module: {err}"), 97 | 98 | Self::UnexpectedImportType { module, name } => { 99 | write!( 100 | formatter, 101 | "unexpected type of import `{module}::{name}`; expected a function" 102 | ) 103 | } 104 | 105 | Self::NoExport(name) => { 106 | write!(formatter, "missing exported function `{name}`") 107 | } 108 | Self::UnexpectedExportType(name) => { 109 | write!( 110 | formatter, 111 | "unexpected type of export `{name}`; expected a function" 112 | ) 113 | } 114 | 115 | Self::UnexpectedArity { 116 | module, 117 | name, 118 | expected_arity, 119 | real_arity, 120 | } => { 121 | let module_descr = module 122 | .as_ref() 123 | .map_or_else(String::new, |module| format!(" imported from `{module}`")); 124 | write!( 125 | formatter, 126 | "unexpected arity for function `{name}`{module_descr}: \ 127 | expected {expected_arity}, got {real_arity}" 128 | ) 129 | } 130 | Self::UnexpectedType { 131 | module, 132 | name, 133 | location, 134 | real_type, 135 | } => { 136 | let module_descr = module 137 | .as_ref() 138 | .map_or_else(String::new, |module| format!(" imported from `{module}`")); 139 | write!( 140 | formatter, 141 | "{location} of function `{name}`{module_descr} has unexpected type; \ 142 | expected `i32`, got {real_type}" 143 | ) 144 | } 145 | 146 | Self::IncorrectGuard { 147 | function_name, 148 | code_offset, 149 | } => { 150 | let function_name = function_name 151 | .as_ref() 152 | .map_or("(unnamed function)", String::as_str); 153 | let code_offset = code_offset 154 | .as_ref() 155 | .map_or_else(String::new, |offset| format!(" at {offset}")); 156 | write!( 157 | formatter, 158 | "incorrectly placed externref guard in {function_name}{code_offset}. \ 159 | {EXTERNAL_TOOL_TIP}" 160 | ) 161 | } 162 | Self::UnexpectedCall { 163 | function_name, 164 | code_offset, 165 | } => { 166 | let function_name = function_name 167 | .as_ref() 168 | .map_or("(unnamed function)", String::as_str); 169 | let code_offset = code_offset 170 | .as_ref() 171 | .map_or_else(String::new, |offset| format!(" at {offset}")); 172 | write!( 173 | formatter, 174 | "unexpected call to an `externref`-returning function \ 175 | in {function_name}{code_offset}. {EXTERNAL_TOOL_TIP}" 176 | ) 177 | } 178 | } 179 | } 180 | } 181 | 182 | impl From for Error { 183 | fn from(err: ReadError) -> Self { 184 | Self::Read(err) 185 | } 186 | } 187 | 188 | impl error::Error for Error { 189 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 190 | match self { 191 | Self::Read(err) => Some(err), 192 | Self::Wasm(err) => Some(err.as_ref()), 193 | _ => None, 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /crates/lib/src/processor/functions.rs: -------------------------------------------------------------------------------- 1 | //! Patched functions for working with `externref`s. 2 | 3 | use std::{ 4 | cmp, 5 | collections::{HashMap, HashSet}, 6 | }; 7 | 8 | use walrus::{ 9 | ir::{self, BinaryOp}, 10 | Function, FunctionBuilder, FunctionId, FunctionKind as WasmFunctionKind, ImportKind, 11 | InstrLocId, InstrSeqBuilder, LocalFunction, LocalId, Module, ModuleImports, RefType, TableId, 12 | ValType, 13 | }; 14 | 15 | use super::{Error, Processor, EXTERNREF}; 16 | 17 | #[derive(Debug)] 18 | pub(crate) struct ExternrefImports { 19 | insert: Option, 20 | get: Option, 21 | drop: Option, 22 | guard: Option, 23 | } 24 | 25 | impl ExternrefImports { 26 | const MODULE_NAME: &'static str = "externref"; 27 | 28 | pub fn new(imports: &mut ModuleImports) -> Result { 29 | Ok(Self { 30 | insert: Self::take_import(imports, "insert")?, 31 | get: Self::take_import(imports, "get")?, 32 | drop: Self::take_import(imports, "drop")?, 33 | guard: Self::take_import(imports, "guard")?, 34 | }) 35 | } 36 | 37 | fn take_import(imports: &mut ModuleImports, name: &str) -> Result, Error> { 38 | let fn_id = imports.find(Self::MODULE_NAME, name).map(|import_id| { 39 | match imports.get(import_id).kind { 40 | ImportKind::Function(fn_id) => { 41 | imports.delete(import_id); 42 | Ok(fn_id) 43 | } 44 | _ => Err(Error::UnexpectedImportType { 45 | module: Self::MODULE_NAME.to_owned(), 46 | name: name.to_owned(), 47 | }), 48 | } 49 | }); 50 | fn_id.transpose() 51 | } 52 | } 53 | 54 | #[derive(Debug)] 55 | pub(crate) struct PatchedFunctions { 56 | fn_mapping: HashMap, 57 | get_ref_id: Option, 58 | guard_id: Option, 59 | } 60 | 61 | impl PatchedFunctions { 62 | #[cfg_attr( 63 | feature = "tracing", 64 | tracing::instrument(level = "debug", name = "patch_imports", skip_all) 65 | )] 66 | pub fn new(module: &mut Module, imports: &ExternrefImports, processor: &Processor<'_>) -> Self { 67 | let table_id = module.tables.add_local(false, 0, None, RefType::Externref); 68 | if let Some(table_name) = processor.table_name { 69 | module.exports.add(table_name, table_id); 70 | } 71 | 72 | let mut fn_mapping = HashMap::with_capacity(3); 73 | let mut get_ref_id = None; 74 | 75 | if let Some(fn_id) = imports.insert { 76 | #[cfg(feature = "tracing")] 77 | tracing::debug!(name = "externref::insert", "replaced import"); 78 | 79 | module.funcs.delete(fn_id); 80 | fn_mapping.insert(fn_id, Self::patch_insert_fn(module, table_id)); 81 | } 82 | 83 | if let Some(fn_id) = imports.get { 84 | #[cfg(feature = "tracing")] 85 | tracing::debug!(name = "externref::get", "replaced import"); 86 | 87 | module.funcs.delete(fn_id); 88 | let patched_fn_id = Self::patch_get_fn(module, table_id); 89 | fn_mapping.insert(fn_id, patched_fn_id); 90 | get_ref_id = Some(patched_fn_id); 91 | } 92 | 93 | if let Some(fn_id) = imports.drop { 94 | #[cfg(feature = "tracing")] 95 | tracing::debug!(name = "externref::drop", "replaced import"); 96 | 97 | module.funcs.delete(fn_id); 98 | let drop_fn_id = processor.drop_fn_name.map(|(module_name, name)| { 99 | let ty = module.types.add(&[EXTERNREF], &[]); 100 | module.add_import_func(module_name, name, ty).0 101 | }); 102 | fn_mapping.insert(fn_id, Self::patch_drop_fn(module, table_id, drop_fn_id)); 103 | } 104 | 105 | Self { 106 | fn_mapping, 107 | get_ref_id, 108 | guard_id: imports.guard, 109 | } 110 | } 111 | 112 | // We want to implement the following logic: 113 | // 114 | // ``` 115 | // if value == NULL { 116 | // return -1; 117 | // } 118 | // let table_len = externrefs_table.len(); 119 | // let mut free_idx; 120 | // if table_len > 0 { 121 | // free_idx -= 1; 122 | // loop { 123 | // if externrefs_table[free_idx] == NULL { 124 | // break; 125 | // } else if free_idx == 0 { 126 | // free_idx = table_len; 127 | // break; 128 | // } else { 129 | // free_idx -= 1; 130 | // } 131 | // } 132 | // } else { 133 | // free_idx = 0; 134 | // }; 135 | // if free_idx == table_len { 136 | // externrefs_table.grow(1, value); 137 | // } else { 138 | // externrefs_table[free_idx] = value; 139 | // } 140 | // free_idx 141 | // ``` 142 | fn patch_insert_fn(module: &mut Module, table_id: TableId) -> FunctionId { 143 | let mut builder = FunctionBuilder::new(&mut module.types, &[EXTERNREF], &[ValType::I32]); 144 | let value = module.locals.add(EXTERNREF); 145 | let free_idx = module.locals.add(ValType::I32); 146 | builder 147 | .func_body() 148 | .local_get(value) 149 | .ref_is_null() 150 | .if_else( 151 | None, 152 | |value_is_null| { 153 | value_is_null.i32_const(-1).return_(); 154 | }, 155 | |_| {}, 156 | ) 157 | .table_size(table_id) 158 | .if_else( 159 | None, 160 | |table_is_not_empty| { 161 | table_is_not_empty 162 | .table_size(table_id) 163 | .i32_const(1) 164 | .binop(BinaryOp::I32Sub) 165 | .local_set(free_idx) 166 | .block(None, |loop_wrapper| { 167 | Self::create_loop(loop_wrapper, table_id, free_idx); 168 | }); 169 | }, 170 | |_| {}, 171 | ) 172 | .local_get(free_idx) 173 | .table_size(table_id) 174 | .binop(BinaryOp::I32Eq) 175 | .if_else( 176 | None, 177 | |growth_required| { 178 | growth_required 179 | .local_get(value) 180 | .i32_const(1) 181 | .table_grow(table_id) 182 | .i32_const(-1) 183 | .binop(BinaryOp::I32Eq) 184 | .if_else( 185 | None, 186 | |growth_failed| { 187 | growth_failed.unreachable(); 188 | }, 189 | |_| {}, 190 | ); 191 | }, 192 | |growth_not_required| { 193 | growth_not_required 194 | .local_get(free_idx) 195 | .local_get(value) 196 | .table_set(table_id); 197 | }, 198 | ) 199 | .local_get(free_idx); 200 | builder.finish(vec![value], &mut module.funcs) 201 | } 202 | 203 | fn create_loop(builder: &mut InstrSeqBuilder<'_>, table_id: TableId, free_idx: LocalId) { 204 | let break_id = builder.id(); 205 | builder.loop_(None, |idx_loop| { 206 | let loop_id = idx_loop.id(); 207 | idx_loop 208 | .local_get(free_idx) 209 | .table_get(table_id) 210 | .ref_is_null() 211 | .if_else( 212 | None, 213 | |is_null| { 214 | is_null.br(break_id); 215 | }, 216 | |is_not_null| { 217 | is_not_null.local_get(free_idx).if_else( 218 | None, 219 | |is_not_zero| { 220 | is_not_zero 221 | .local_get(free_idx) 222 | .i32_const(1) 223 | .binop(BinaryOp::I32Sub) 224 | .local_set(free_idx) 225 | .br(loop_id); 226 | }, 227 | |is_zero| { 228 | is_zero 229 | .table_size(table_id) 230 | .local_set(free_idx) 231 | .br(break_id); 232 | }, 233 | ); 234 | }, 235 | ); 236 | }); 237 | } 238 | 239 | fn patch_get_fn(module: &mut Module, table_id: TableId) -> FunctionId { 240 | let mut builder = FunctionBuilder::new(&mut module.types, &[ValType::I32], &[EXTERNREF]); 241 | let idx = module.locals.add(ValType::I32); 242 | builder 243 | .func_body() 244 | .local_get(idx) 245 | .i32_const(-1) 246 | .binop(BinaryOp::I32Eq) 247 | .if_else( 248 | EXTERNREF, 249 | |null_requested| { 250 | null_requested.ref_null(RefType::Externref); 251 | }, 252 | |elem_requested| { 253 | elem_requested.local_get(idx).table_get(table_id); 254 | }, 255 | ); 256 | builder.finish(vec![idx], &mut module.funcs) 257 | } 258 | 259 | fn patch_drop_fn( 260 | module: &mut Module, 261 | table_id: TableId, 262 | drop_fn_id: Option, 263 | ) -> FunctionId { 264 | let mut builder = FunctionBuilder::new(&mut module.types, &[ValType::I32], &[]); 265 | let idx = module.locals.add(ValType::I32); 266 | 267 | let mut instr_builder = builder.func_body(); 268 | if let Some(drop_fn_id) = drop_fn_id { 269 | instr_builder 270 | .local_get(idx) 271 | .table_get(table_id) 272 | .call(drop_fn_id); 273 | } 274 | instr_builder 275 | .local_get(idx) 276 | .ref_null(RefType::Externref) 277 | .table_set(table_id); 278 | builder.finish(vec![idx], &mut module.funcs) 279 | } 280 | 281 | pub fn get_ref_id(&self) -> Option { 282 | self.get_ref_id 283 | } 284 | 285 | pub fn replace_calls( 286 | &self, 287 | module: &mut Module, 288 | ) -> Result<(usize, HashSet), Error> { 289 | let mut visitor = FunctionsReplacer::new(&self.fn_mapping); 290 | let mut guarded_fns = HashSet::new(); 291 | for function in module.funcs.iter_mut() { 292 | if let WasmFunctionKind::Local(local_fn) = &mut function.kind { 293 | ir::dfs_pre_order_mut(&mut visitor, local_fn, local_fn.entry_block()); 294 | 295 | if let Some(guard_id) = self.guard_id { 296 | if Self::remove_guards(guard_id, function)? { 297 | guarded_fns.insert(function.id()); 298 | } 299 | } 300 | } 301 | } 302 | Ok((visitor.replaced_count, guarded_fns)) 303 | } 304 | 305 | fn remove_guards(guard_id: FunctionId, function: &mut Function) -> Result { 306 | let local_fn = function.kind.unwrap_local_mut(); 307 | let mut guard_visitor = GuardRemover::new(guard_id, local_fn); 308 | ir::dfs_pre_order_mut(&mut guard_visitor, local_fn, local_fn.entry_block()); 309 | match guard_visitor.placement { 310 | None => Ok(false), 311 | Some(GuardPlacement::Correct) => Ok(true), 312 | Some(GuardPlacement::Incorrect(code_offset)) => Err(Error::IncorrectGuard { 313 | function_name: function.name.clone(), 314 | code_offset, 315 | }), 316 | } 317 | } 318 | } 319 | 320 | /// Visitor replacing invocations of patched functions. 321 | #[derive(Debug)] 322 | struct FunctionsReplacer<'a> { 323 | fn_mapping: &'a HashMap, 324 | replaced_count: usize, 325 | } 326 | 327 | impl<'a> FunctionsReplacer<'a> { 328 | fn new(fn_mapping: &'a HashMap) -> Self { 329 | Self { 330 | fn_mapping, 331 | replaced_count: 0, 332 | } 333 | } 334 | } 335 | 336 | impl ir::VisitorMut for FunctionsReplacer<'_> { 337 | fn visit_function_id_mut(&mut self, function: &mut FunctionId) { 338 | if let Some(mapped_id) = self.fn_mapping.get(function) { 339 | *function = *mapped_id; 340 | self.replaced_count += 1; 341 | } 342 | } 343 | } 344 | 345 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 346 | enum GuardPlacement { 347 | Correct, 348 | // The encapsulated value is the WASM offset. 349 | Incorrect(Option), 350 | } 351 | 352 | /// Visitor removing invocations of a certain function. 353 | struct GuardRemover { 354 | guard_id: FunctionId, 355 | entry_seq_id: ir::InstrSeqId, 356 | placement: Option, 357 | } 358 | 359 | impl GuardRemover { 360 | fn new(guard_id: FunctionId, local_fn: &LocalFunction) -> Self { 361 | Self { 362 | guard_id, 363 | entry_seq_id: local_fn.entry_block(), 364 | placement: None, 365 | } 366 | } 367 | 368 | fn add_placement(&mut self, placement: GuardPlacement) { 369 | self.placement = cmp::max(self.placement, Some(placement)); 370 | } 371 | } 372 | 373 | impl ir::VisitorMut for GuardRemover { 374 | fn start_instr_seq_mut(&mut self, instr_seq: &mut ir::InstrSeq) { 375 | let is_entry_seq = instr_seq.id() == self.entry_seq_id; 376 | let mut idx = 0; 377 | let mut maybe_set_stack_ptr = false; 378 | instr_seq.instrs.retain(|(instr, location)| { 379 | let placement = if let ir::Instr::Call(call) = instr { 380 | if call.func == self.guard_id { 381 | Some(if is_entry_seq && (idx == 0 || maybe_set_stack_ptr) { 382 | GuardPlacement::Correct 383 | } else { 384 | GuardPlacement::Incorrect(get_offset(*location)) 385 | }) 386 | } else { 387 | None 388 | } 389 | } else { 390 | None 391 | }; 392 | 393 | if let Some(placement) = placement { 394 | self.add_placement(placement); 395 | } 396 | idx += 1; 397 | maybe_set_stack_ptr = matches!(instr, ir::Instr::GlobalSet(_)); 398 | placement.is_none() 399 | }); 400 | } 401 | } 402 | 403 | /// Gets WASM bytecode offset. 404 | pub(crate) fn get_offset(location: InstrLocId) -> Option { 405 | if location.is_default() { 406 | None 407 | } else { 408 | Some(location.data()) 409 | } 410 | } 411 | 412 | #[cfg(test)] 413 | mod tests { 414 | use assert_matches::assert_matches; 415 | 416 | use super::*; 417 | 418 | #[test] 419 | fn taking_externref_imports() { 420 | const MODULE_BYTES: &[u8] = br#" 421 | (module 422 | (import "externref" "insert" (func (param i32) (result i32))) 423 | (import "externref" "get" (func (param i32) (result i32))) 424 | (import "test" "function" (func (param f32))) 425 | ) 426 | "#; 427 | 428 | let module = wat::parse_bytes(MODULE_BYTES).unwrap(); 429 | let mut module = Module::from_buffer(&module).unwrap(); 430 | 431 | let imports = ExternrefImports::new(&mut module.imports).unwrap(); 432 | assert!(imports.insert.is_some()); 433 | assert!(imports.get.is_some()); 434 | assert!(imports.drop.is_none()); 435 | assert_eq!(module.imports.iter().count(), 1); 436 | } 437 | 438 | #[test] 439 | fn replacing_function_calls() { 440 | const MODULE_BYTES: &[u8] = br#" 441 | (module 442 | (import "externref" "insert" (func $insert_ref (param i32) (result i32))) 443 | (import "externref" "get" (func $get_ref (param i32) (result i32))) 444 | 445 | (func (export "test") (param $ref i32) 446 | (drop (call $get_ref 447 | (call $insert_ref (local.get $ref)) 448 | )) 449 | ) 450 | ) 451 | "#; 452 | 453 | let module = wat::parse_bytes(MODULE_BYTES).unwrap(); 454 | let mut module = Module::from_buffer(&module).unwrap(); 455 | let imports = ExternrefImports::new(&mut module.imports).unwrap(); 456 | 457 | let fns = PatchedFunctions::new(&mut module, &imports, &Processor::default()); 458 | assert_eq!(fns.fn_mapping.len(), 2); 459 | let (replaced_calls, guarded_fns) = fns.replace_calls(&mut module).unwrap(); 460 | assert_eq!(replaced_calls, 2); // 1 insert + 1 get 461 | assert!(guarded_fns.is_empty()); 462 | } 463 | 464 | #[test] 465 | fn guarded_functions() { 466 | const MODULE_BYTES: &[u8] = br#" 467 | (module 468 | (import "externref" "guard" (func $guard)) 469 | 470 | (func (param $ref i32) 471 | (call $guard) 472 | (drop (local.get $ref)) 473 | ) 474 | ) 475 | "#; 476 | 477 | let module = wat::parse_bytes(MODULE_BYTES).unwrap(); 478 | let mut module = Module::from_buffer(&module).unwrap(); 479 | let imports = ExternrefImports::new(&mut module.imports).unwrap(); 480 | 481 | let fns = PatchedFunctions::new(&mut module, &imports, &Processor::default()); 482 | let (_, guarded_fns) = fns.replace_calls(&mut module).unwrap(); 483 | assert_eq!(guarded_fns.len(), 1); 484 | } 485 | 486 | #[test] 487 | fn guarded_function_manipulating_stack() { 488 | const MODULE_BYTES: &[u8] = br#" 489 | (module 490 | (import "externref" "guard" (func $guard)) 491 | (global $__stack_pointer (mut i32) (i32.const 32768)) 492 | 493 | (func (param $ref i32) 494 | (local $0 i32) 495 | (global.set $__stack_pointer 496 | (local.tee $0 497 | (i32.sub (global.get $__stack_pointer) (i32.const 16)) 498 | ) 499 | ) 500 | (call $guard) 501 | (drop (local.get $ref)) 502 | ) 503 | ) 504 | "#; 505 | 506 | let module = wat::parse_bytes(MODULE_BYTES).unwrap(); 507 | let mut module = Module::from_buffer(&module).unwrap(); 508 | let imports = ExternrefImports::new(&mut module.imports).unwrap(); 509 | 510 | let fns = PatchedFunctions::new(&mut module, &imports, &Processor::default()); 511 | let (_, guarded_fns) = fns.replace_calls(&mut module).unwrap(); 512 | assert_eq!(guarded_fns.len(), 1); 513 | } 514 | 515 | #[test] 516 | fn incorrect_guard_placement() { 517 | const MODULE_BYTES: &[u8] = br#" 518 | (module 519 | (import "externref" "guard" (func $guard)) 520 | 521 | (func $test (param $ref i32) 522 | (drop (local.get $ref)) 523 | (call $guard) 524 | ) 525 | ) 526 | "#; 527 | 528 | let module = wat::parse_bytes(MODULE_BYTES).unwrap(); 529 | let mut module = Module::from_buffer(&module).unwrap(); 530 | let imports = ExternrefImports::new(&mut module.imports).unwrap(); 531 | 532 | let fns = PatchedFunctions::new(&mut module, &imports, &Processor::default()); 533 | let err = fns.replace_calls(&mut module).unwrap_err(); 534 | assert_matches!( 535 | err, 536 | Error::IncorrectGuard { function_name: Some(name), .. } if name == "test" 537 | ); 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /crates/lib/src/processor/mod.rs: -------------------------------------------------------------------------------- 1 | //! WASM module processor for `externref`s. 2 | //! 3 | //! WASM modules that use the `externref` crate need to be processed in order 4 | //! to use `externref` function args / return types for imported or exported functions that 5 | //! originally used [`Resource`](crate::Resource)s. This module encapsulates processing logic. 6 | //! 7 | //! More precisely, the processor performs the following steps: 8 | //! 9 | //! - Parse the custom section with [`Function`] declarations and remove this section 10 | //! from the module. 11 | //! - Replace imported functions from a surrogate module for handling `externref`s with 12 | //! local functions. 13 | //! - Patch signatures and implementations of imported / exported functions so that they 14 | //! use `externref`s where appropriate. 15 | //! - Add an initially empty, unconstrained table with `externref` elements and optionally 16 | //! export it from the module. The host can use the table to inspect currently used references 17 | //! (e.g., to save / restore WASM instance state). 18 | //! 19 | //! See [crate-level docs](..) for more insights on WASM module setup and processing. 20 | //! 21 | //! # On processing order 22 | //! 23 | //! ⚠ **Important.** The [`Processor`] should run *before* WASM optimization tools such as `wasm-opt`. 24 | //! These tools may inline `externref`-operating functions, which can lead to the processor 25 | //! producing invalid WASM bytecode (roughly speaking, excessively replacing `i32`s 26 | //! with `externref`s). Such inlining can usually be detected by the processor, in which case 27 | //! it will return [`Error::IncorrectGuard`] or [`Error::UnexpectedCall`] 28 | //! from [`process()`](Processor::process()). 29 | //! 30 | //! Optimizing WASM after the processor has an additional advantage in that it can 31 | //! optimize the changes produced by it (optimization is hard, and is best left 32 | //! to the dedicated tools). 33 | //! 34 | //! # Examples 35 | //! 36 | //! ``` 37 | //! use externref::processor::Processor; 38 | //! 39 | //! let module: Vec = // WASM module, e.g., loaded from the file system 40 | //! # b"\0asm\x01\0\0\0".to_vec(); 41 | //! let processed: Vec = Processor::default() 42 | //! // Set a hook to be called when a reference is dropped 43 | //! .set_drop_fn("test", "drop_ref") 44 | //! .process_bytes(&module)?; 45 | //! // Store or use the processed module... 46 | //! # Ok::<_, externref::processor::Error>(()) 47 | //! ``` 48 | 49 | use walrus::{passes::gc, Module, RefType, ValType}; 50 | 51 | pub use self::error::{Error, Location}; 52 | use self::state::ProcessingState; 53 | use crate::Function; 54 | 55 | mod error; 56 | mod functions; 57 | mod state; 58 | 59 | /// Externref type as a constant. 60 | const EXTERNREF: ValType = ValType::Ref(RefType::Externref); 61 | 62 | /// WASM module processor encapsulating processing options. 63 | #[derive(Debug)] 64 | pub struct Processor<'a> { 65 | table_name: Option<&'a str>, 66 | drop_fn_name: Option<(&'a str, &'a str)>, 67 | } 68 | 69 | impl Default for Processor<'_> { 70 | fn default() -> Self { 71 | Self { 72 | table_name: Some("externrefs"), 73 | drop_fn_name: None, 74 | } 75 | } 76 | } 77 | 78 | impl<'a> Processor<'a> { 79 | /// Sets the name of the exported `externref`s table where refs obtained from the host 80 | /// are placed. If set to `None`, the table will not be exported from the module. 81 | /// 82 | /// By default, the table is exported as `"externrefs"`. 83 | pub fn set_ref_table(&mut self, name: impl Into>) -> &mut Self { 84 | self.table_name = name.into(); 85 | self 86 | } 87 | 88 | /// Sets a function to notify the host about dropped `externref`s. This function 89 | /// will be added as an import with a signature `(externref) -> ()` and will be called 90 | /// immediately before dropping each reference. 91 | /// 92 | /// By default, there is no notifier hook installed. 93 | pub fn set_drop_fn(&mut self, module: &'a str, name: &'a str) -> &mut Self { 94 | self.drop_fn_name = Some((module, name)); 95 | self 96 | } 97 | 98 | /// Processes the provided `module`. 99 | /// 100 | /// # Errors 101 | /// 102 | /// Returns an error if a module is malformed. This shouldn't normally happen and 103 | /// could be caused by another post-processor or a bug in the `externref` crate / proc macro. 104 | #[cfg_attr(feature = "tracing", tracing::instrument(skip_all, err))] 105 | pub fn process(&self, module: &mut Module) -> Result<(), Error> { 106 | let raw_section = module.customs.remove_raw(Function::CUSTOM_SECTION_NAME); 107 | let Some(raw_section) = raw_section else { 108 | #[cfg(feature = "tracing")] 109 | tracing::info!("module contains no custom section; skipping"); 110 | return Ok(()); 111 | }; 112 | let functions = Self::parse_section(&raw_section.data)?; 113 | #[cfg(feature = "tracing")] 114 | tracing::info!(functions.len = functions.len(), "parsed custom section"); 115 | 116 | let state = ProcessingState::new(module, self)?; 117 | let guarded_fns = state.replace_functions(module)?; 118 | state.process_functions(&functions, &guarded_fns, module)?; 119 | 120 | gc::run(module); 121 | Ok(()) 122 | } 123 | 124 | fn parse_section(mut raw_section: &[u8]) -> Result>, Error> { 125 | let mut functions = vec![]; 126 | while !raw_section.is_empty() { 127 | let next_function = Function::read_from_section(&mut raw_section)?; 128 | functions.push(next_function); 129 | } 130 | Ok(functions) 131 | } 132 | 133 | /// Processes the provided WASM module `bytes`. This is a higher-level alternative to 134 | /// [`Self::process()`]. 135 | /// 136 | /// # Errors 137 | /// 138 | /// Returns an error if `bytes` does not represent a valid WASM module, and in all cases 139 | /// [`Self::process()`] returns an error. 140 | pub fn process_bytes(&self, bytes: &[u8]) -> Result, Error> { 141 | let mut module = Module::from_buffer(bytes).map_err(Error::Wasm)?; 142 | self.process(&mut module)?; 143 | Ok(module.emit_wasm()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /crates/lib/src/signature.rs: -------------------------------------------------------------------------------- 1 | //! Function signatures recorded into a custom section of WASM modules. 2 | 3 | use core::str; 4 | 5 | use crate::{ 6 | alloc::{format, String}, 7 | error::{ReadError, ReadErrorKind}, 8 | }; 9 | 10 | /// Builder for [`BitSlice`]s that can be used in const contexts. 11 | #[doc(hidden)] // used by macro; not public (yet?) 12 | #[derive(Debug)] 13 | pub struct BitSliceBuilder { 14 | bytes: [u8; BYTES], 15 | bit_len: usize, 16 | } 17 | 18 | #[doc(hidden)] // not public yet 19 | impl BitSliceBuilder { 20 | #[must_use] 21 | pub const fn with_set_bit(mut self, bit_idx: usize) -> Self { 22 | assert!(bit_idx < self.bit_len); 23 | self.bytes[bit_idx / 8] |= 1 << (bit_idx % 8); 24 | self 25 | } 26 | 27 | pub const fn build(&self) -> BitSlice<'_> { 28 | BitSlice { 29 | bytes: &self.bytes, 30 | bit_len: self.bit_len, 31 | } 32 | } 33 | } 34 | 35 | /// Slice of bits. This type is used to mark [`Resource`](crate::Resource) args 36 | /// in imported / exported functions. 37 | // Why invent a new type? Turns out that existing implementations (e.g., `bv` and `bitvec`) 38 | // cannot be used in const contexts. 39 | #[derive(Debug, Clone, Copy)] 40 | #[cfg_attr(test, derive(PartialEq, Eq))] 41 | pub struct BitSlice<'a> { 42 | bytes: &'a [u8], 43 | bit_len: usize, 44 | } 45 | 46 | impl BitSlice<'static> { 47 | #[doc(hidden)] 48 | pub const fn builder(bit_len: usize) -> BitSliceBuilder { 49 | assert!(BYTES > 0); 50 | assert!(bit_len > (BYTES - 1) * 8 && bit_len <= BYTES * 8); 51 | BitSliceBuilder { 52 | bytes: [0_u8; BYTES], 53 | bit_len, 54 | } 55 | } 56 | } 57 | 58 | impl<'a> BitSlice<'a> { 59 | /// Returns the number of bits in this slice. 60 | pub fn bit_len(&self) -> usize { 61 | self.bit_len 62 | } 63 | 64 | /// Checks if a bit with the specified 0-based index is set. 65 | pub fn is_set(&self, idx: usize) -> bool { 66 | if idx > self.bit_len { 67 | return false; 68 | } 69 | let mask = 1 << (idx % 8); 70 | self.bytes[idx / 8] & mask > 0 71 | } 72 | 73 | /// Iterates over the indexes of set bits in this slice. 74 | pub fn set_indices(&self) -> impl Iterator + '_ { 75 | (0..self.bit_len).filter(|&idx| self.is_set(idx)) 76 | } 77 | 78 | /// Returns the number of set bits in this slice. 79 | pub fn count_ones(&self) -> usize { 80 | let ones: u32 = self.bytes.iter().copied().map(u8::count_ones).sum(); 81 | ones as usize 82 | } 83 | 84 | fn read_from_section(buffer: &mut &'a [u8], context: &str) -> Result { 85 | let bit_len = read_u32(buffer, || format!("length for {context}"))? as usize; 86 | let byte_len = bit_len.div_ceil(8); 87 | if buffer.len() < byte_len { 88 | Err(ReadErrorKind::UnexpectedEof.with_context(context)) 89 | } else { 90 | let bytes = &buffer[..byte_len]; 91 | *buffer = &buffer[byte_len..]; 92 | Ok(Self { bytes, bit_len }) 93 | } 94 | } 95 | } 96 | 97 | macro_rules! write_u32 { 98 | ($buffer:ident, $value:expr, $pos:expr) => {{ 99 | let value: u32 = $value; 100 | let pos: usize = $pos; 101 | $buffer[pos] = (value & 0xff) as u8; 102 | $buffer[pos + 1] = ((value >> 8) & 0xff) as u8; 103 | $buffer[pos + 2] = ((value >> 16) & 0xff) as u8; 104 | $buffer[pos + 3] = ((value >> 24) & 0xff) as u8; 105 | }}; 106 | } 107 | 108 | fn read_u32(buffer: &mut &[u8], context: impl FnOnce() -> String) -> Result { 109 | if buffer.len() < 4 { 110 | Err(ReadErrorKind::UnexpectedEof.with_context(context())) 111 | } else { 112 | let value = u32::from_le_bytes(buffer[..4].try_into().unwrap()); 113 | *buffer = &buffer[4..]; 114 | Ok(value) 115 | } 116 | } 117 | 118 | fn read_str<'a>(buffer: &mut &'a [u8], context: &str) -> Result<&'a str, ReadError> { 119 | let len = read_u32(buffer, || format!("length for {context}"))? as usize; 120 | if buffer.len() < len { 121 | Err(ReadErrorKind::UnexpectedEof.with_context(context)) 122 | } else { 123 | let string = str::from_utf8(&buffer[..len]) 124 | .map_err(|err| ReadErrorKind::Utf8(err).with_context(context))?; 125 | *buffer = &buffer[len..]; 126 | Ok(string) 127 | } 128 | } 129 | 130 | /// Kind of a function with [`Resource`](crate::Resource) args or return type. 131 | #[derive(Debug)] 132 | #[cfg_attr(test, derive(PartialEq, Eq))] 133 | pub enum FunctionKind<'a> { 134 | /// Function exported from a WASM module. 135 | Export, 136 | /// Function imported to a WASM module from the module with the enclosed name. 137 | Import(&'a str), 138 | } 139 | 140 | impl<'a> FunctionKind<'a> { 141 | const fn len_in_custom_section(&self) -> usize { 142 | match self { 143 | Self::Export => 4, 144 | Self::Import(module_name) => 4 + module_name.len(), 145 | } 146 | } 147 | 148 | #[allow(clippy::cast_possible_truncation)] // `TryFrom` cannot be used in const fns 149 | const fn write_to_custom_section( 150 | &self, 151 | mut buffer: [u8; N], 152 | ) -> ([u8; N], usize) { 153 | match self { 154 | Self::Export => { 155 | write_u32!(buffer, u32::MAX, 0); 156 | (buffer, 4) 157 | } 158 | 159 | Self::Import(module_name) => { 160 | write_u32!(buffer, module_name.len() as u32, 0); 161 | let mut pos = 4; 162 | while pos - 4 < module_name.len() { 163 | buffer[pos] = module_name.as_bytes()[pos - 4]; 164 | pos += 1; 165 | } 166 | (buffer, pos) 167 | } 168 | } 169 | } 170 | 171 | fn read_from_section(buffer: &mut &'a [u8]) -> Result { 172 | if buffer.len() >= 4 && buffer[..4] == [0xff; 4] { 173 | *buffer = &buffer[4..]; 174 | Ok(Self::Export) 175 | } else { 176 | let module_name = read_str(buffer, "module name")?; 177 | Ok(Self::Import(module_name)) 178 | } 179 | } 180 | } 181 | 182 | /// Information about a function with [`Resource`](crate::Resource) args or return type. 183 | /// 184 | /// This information is written to a custom section of a WASM module and is then used 185 | /// during module [post-processing]. 186 | /// 187 | /// [post-processing]: crate::processor 188 | #[derive(Debug)] 189 | #[cfg_attr(test, derive(PartialEq, Eq))] 190 | pub struct Function<'a> { 191 | /// Kind of this function. 192 | pub kind: FunctionKind<'a>, 193 | /// Name of this function. 194 | pub name: &'a str, 195 | /// Bit slice marking [`Resource`](crate::Resource) args / return type. 196 | pub externrefs: BitSlice<'a>, 197 | } 198 | 199 | impl<'a> Function<'a> { 200 | /// Name of a custom section in WASM modules where `Function` declarations are stored. 201 | /// `Function`s can be read from this section using [`Self::read_from_section()`]. 202 | // **NB.** Keep synced with the `declare_function!()` macro below. 203 | pub const CUSTOM_SECTION_NAME: &'static str = "__externrefs"; 204 | 205 | /// Computes length of a custom section for this function signature. 206 | #[doc(hidden)] 207 | pub const fn custom_section_len(&self) -> usize { 208 | self.kind.len_in_custom_section() + 4 + self.name.len() + 4 + self.externrefs.bytes.len() 209 | } 210 | 211 | #[doc(hidden)] 212 | #[allow(clippy::cast_possible_truncation)] // `TryFrom` cannot be used in const fns 213 | pub const fn custom_section(&self) -> [u8; N] { 214 | debug_assert!(N == self.custom_section_len()); 215 | let (mut buffer, mut pos) = self.kind.write_to_custom_section([0_u8; N]); 216 | 217 | write_u32!(buffer, self.name.len() as u32, pos); 218 | pos += 4; 219 | let mut i = 0; 220 | while i < self.name.len() { 221 | buffer[pos] = self.name.as_bytes()[i]; 222 | pos += 1; 223 | i += 1; 224 | } 225 | 226 | write_u32!(buffer, self.externrefs.bit_len as u32, pos); 227 | pos += 4; 228 | let mut i = 0; 229 | while i < self.externrefs.bytes.len() { 230 | buffer[pos] = self.externrefs.bytes[i]; 231 | i += 1; 232 | pos += 1; 233 | } 234 | 235 | buffer 236 | } 237 | 238 | /// Reads function information from a WASM custom section. After reading, the `buffer` 239 | /// is advanced to trim the bytes consumed by the parser. 240 | /// 241 | /// This crate does not provide tools to read custom sections from a WASM module; 242 | /// use a library like [`walrus`] or [`wasmparser`] for this purpose. 243 | /// 244 | /// # Errors 245 | /// 246 | /// Returns an error if the custom section is malformed. 247 | /// 248 | /// [`walrus`]: https://docs.rs/walrus/ 249 | /// [`wasmparser`]: https://docs.rs/wasmparser/ 250 | pub fn read_from_section(buffer: &mut &'a [u8]) -> Result { 251 | let kind = FunctionKind::read_from_section(buffer)?; 252 | Ok(Self { 253 | kind, 254 | name: read_str(buffer, "function name")?, 255 | externrefs: BitSlice::read_from_section(buffer, "externref bit slice")?, 256 | }) 257 | } 258 | } 259 | 260 | #[macro_export] 261 | #[doc(hidden)] 262 | macro_rules! declare_function { 263 | ($signature:expr) => { 264 | const _: () = { 265 | const FUNCTION: $crate::Function = $signature; 266 | 267 | #[cfg_attr(target_arch = "wasm32", link_section = "__externrefs")] 268 | static DATA_SECTION: [u8; FUNCTION.custom_section_len()] = FUNCTION.custom_section(); 269 | }; 270 | }; 271 | } 272 | 273 | #[cfg(test)] 274 | mod tests { 275 | use super::*; 276 | 277 | #[test] 278 | fn function_serialization() { 279 | const FUNCTION: Function = Function { 280 | kind: FunctionKind::Import("module"), 281 | name: "test", 282 | externrefs: BitSlice::builder::<1>(3).with_set_bit(1).build(), 283 | }; 284 | 285 | const SECTION: [u8; FUNCTION.custom_section_len()] = FUNCTION.custom_section(); 286 | 287 | assert_eq!(SECTION[..4], [6, 0, 0, 0]); // little-endian module name length 288 | assert_eq!(SECTION[4..10], *b"module"); 289 | assert_eq!(SECTION[10..14], [4, 0, 0, 0]); // little-endian fn name length 290 | assert_eq!(SECTION[14..18], *b"test"); 291 | assert_eq!(SECTION[18..22], [3, 0, 0, 0]); // little-endian bit slice length 292 | assert_eq!(SECTION[22], 2); // bit slice 293 | 294 | let mut section_reader = &SECTION as &[u8]; 295 | let restored_function = Function::read_from_section(&mut section_reader).unwrap(); 296 | assert_eq!(restored_function, FUNCTION); 297 | } 298 | 299 | #[test] 300 | fn export_fn_serialization() { 301 | const FUNCTION: Function = Function { 302 | kind: FunctionKind::Export, 303 | name: "test", 304 | externrefs: BitSlice::builder::<1>(3).with_set_bit(1).build(), 305 | }; 306 | 307 | const SECTION: [u8; FUNCTION.custom_section_len()] = FUNCTION.custom_section(); 308 | 309 | assert_eq!(SECTION[..4], [0xff, 0xff, 0xff, 0xff]); 310 | assert_eq!(SECTION[4..8], [4, 0, 0, 0]); // little-endian fn name length 311 | assert_eq!(SECTION[8..12], *b"test"); 312 | 313 | let mut section_reader = &SECTION as &[u8]; 314 | let restored_function = Function::read_from_section(&mut section_reader).unwrap(); 315 | assert_eq!(restored_function, FUNCTION); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /crates/lib/tests/modules/simple-no-inline.wast: -------------------------------------------------------------------------------- 1 | (module 2 | ;; Corresponds to the following logic: 3 | ;; 4 | ;; ``` 5 | ;; extern "C" { 6 | ;; fn alloc(arena: &Resource, cap: usize) 7 | ;; -> Option>; 8 | ;; } 9 | ;; 10 | ;; pub extern "C" fn test(arena: &Resource) { 11 | ;; let _bytes = unsafe { alloc(arena, 42) }.unwrap(); 12 | ;; } 13 | ;; ``` 14 | ;; 15 | ;; Unlike with the module in `simple.wast`, we don't inline some assignments 16 | ;; from functions returning `externref`s in order to test that the corresponding 17 | ;; locals are transformed. 18 | 19 | ;; surrogate imports 20 | (import "externref" "insert" (func $insert_ref (param i32) (result i32))) 21 | (import "externref" "get" (func $get_ref (param i32) (result i32))) 22 | (import "externref" "drop" (func $drop_ref (param i32))) 23 | (import "externref" "guard" (func $ref_guard)) 24 | ;; real imported fn 25 | (import "arena" "alloc" (func $alloc (param i32 i32) (result i32))) 26 | 27 | ;; exported fn 28 | (func (export "test") (param $arena i32) 29 | (local $bytes i32) 30 | (local.set $bytes 31 | (call $alloc 32 | (call $get_ref 33 | (local.tee $arena 34 | (call $insert_ref (local.get $arena)) 35 | ) 36 | ) 37 | (i32.const 42) 38 | ) 39 | ) 40 | (if (i32.eq 41 | (local.tee $bytes 42 | (call $insert_ref (local.get $bytes)) 43 | ) 44 | (i32.const -1)) 45 | (then (unreachable)) 46 | (else (call $drop_ref (local.get $bytes))) 47 | ) 48 | (call $drop_ref (local.get $arena)) 49 | ) 50 | 51 | ;; internal fn; the `ref` local should be transformed as well 52 | (func (param $index i32) 53 | (local $ref i32) 54 | (call $ref_guard) 55 | (local.set $ref 56 | (call $get_ref (local.get $index)) 57 | ) 58 | (call $drop_ref (local.get $ref)) 59 | ) 60 | ) 61 | -------------------------------------------------------------------------------- /crates/lib/tests/modules/simple.wast: -------------------------------------------------------------------------------- 1 | (module 2 | ;; Corresponds to the following logic: 3 | ;; 4 | ;; ``` 5 | ;; extern "C" { 6 | ;; fn alloc(arena: &Resource, cap: usize) 7 | ;; -> Option>; 8 | ;; } 9 | ;; 10 | ;; pub extern "C" fn test(arena: &Resource) { 11 | ;; let _bytes = unsafe { alloc(arena, 42) }.unwrap(); 12 | ;; } 13 | ;; ``` 14 | 15 | ;; surrogate imports 16 | (import "externref" "insert" (func $insert_ref (param i32) (result i32))) 17 | (import "externref" "get" (func $get_ref (param i32) (result i32))) 18 | (import "externref" "drop" (func $drop_ref (param i32))) 19 | ;; real imported fn 20 | (import "arena" "alloc" (func $alloc (param i32 i32) (result i32))) 21 | 22 | ;; exported fn 23 | (func (export "test") (param $arena i32) 24 | (local $bytes i32) 25 | (if (i32.eq 26 | (local.tee $bytes 27 | (call $insert_ref 28 | (call $alloc 29 | (call $get_ref 30 | ;; Reassigning the param local is completely valid, 31 | ;; and the Rust compliler frequently does this. 32 | (local.tee $arena 33 | (call $insert_ref (local.get $arena)) 34 | ) 35 | ) 36 | (i32.const 42) 37 | ) 38 | ) 39 | ) 40 | (i32.const -1)) 41 | (then (unreachable)) 42 | (else (call $drop_ref (local.get $bytes))) 43 | ) 44 | (call $drop_ref (local.get $arena)) 45 | ) 46 | ) 47 | -------------------------------------------------------------------------------- /crates/lib/tests/processor.rs: -------------------------------------------------------------------------------- 1 | //! Tests for processor logic. 2 | 3 | use std::path::Path; 4 | 5 | use externref::{processor::Processor, BitSlice, Function, FunctionKind}; 6 | use walrus::{ExportItem, ImportKind, Module, RawCustomSection, RefType, ValType}; 7 | 8 | const EXTERNREF: ValType = ValType::Ref(RefType::Externref); 9 | 10 | const ARENA_ALLOC: Function<'static> = Function { 11 | kind: FunctionKind::Import("arena"), 12 | name: "alloc", 13 | externrefs: BitSlice::builder::<1>(3) 14 | .with_set_bit(0) 15 | .with_set_bit(2) 16 | .build(), 17 | }; 18 | const ARENA_ALLOC_BYTES: [u8; ARENA_ALLOC.custom_section_len()] = ARENA_ALLOC.custom_section(); 19 | 20 | const TEST: Function<'static> = Function { 21 | kind: FunctionKind::Export, 22 | name: "test", 23 | externrefs: BitSlice::builder::<1>(1).with_set_bit(0).build(), 24 | }; 25 | const TEST_BYTES: [u8; TEST.custom_section_len()] = TEST.custom_section(); 26 | 27 | fn simple_module_path() -> &'static Path { 28 | Path::new("tests/modules/simple.wast") 29 | } 30 | 31 | fn no_inline_module_path() -> &'static Path { 32 | Path::new("tests/modules/simple-no-inline.wast") 33 | } 34 | 35 | fn add_basic_custom_section(module: &mut Module) { 36 | let mut section_data = Vec::with_capacity(ARENA_ALLOC_BYTES.len() + TEST_BYTES.len()); 37 | section_data.extend_from_slice(&ARENA_ALLOC_BYTES); 38 | section_data.extend_from_slice(&TEST_BYTES); 39 | module.customs.add(RawCustomSection { 40 | name: Function::CUSTOM_SECTION_NAME.to_owned(), 41 | data: section_data, 42 | }); 43 | } 44 | 45 | #[test] 46 | fn basic_module() { 47 | let module = wat::parse_file(simple_module_path()).unwrap(); 48 | let mut module = Module::from_buffer(&module).unwrap(); 49 | // We need to add a custom section to the module before processing. 50 | add_basic_custom_section(&mut module); 51 | 52 | Processor::default().process(&mut module).unwrap(); 53 | 54 | // Check that the module has the expected interface. 55 | assert_eq!(module.imports.iter().count(), 1, "{:?}", module.imports); 56 | let import_id = module.imports.find("arena", "alloc").unwrap(); 57 | let import_id = match &module.imports.get(import_id).kind { 58 | ImportKind::Function(fn_id) => *fn_id, 59 | other => panic!("unexpected import type: {other:?}"), 60 | }; 61 | let function_type = module.types.get(module.funcs.get(import_id).ty()); 62 | assert_eq!(function_type.params(), [EXTERNREF, ValType::I32]); 63 | assert_eq!(function_type.results(), [EXTERNREF]); 64 | 65 | assert!(module.exports.iter().any(|export| { 66 | export.name == "externrefs" && matches!(export.item, ExportItem::Table(_)) 67 | })); 68 | 69 | let export_id = module 70 | .exports 71 | .iter() 72 | .find_map(|export| { 73 | if export.name == "test" { 74 | Some(match &export.item { 75 | ExportItem::Function(fn_id) => *fn_id, 76 | other => panic!("unexpected export type: {other:?}"), 77 | }) 78 | } else { 79 | None 80 | } 81 | }) 82 | .unwrap(); 83 | let function_type = module.types.get(module.funcs.get(export_id).ty()); 84 | assert_eq!(function_type.params(), [EXTERNREF]); 85 | assert_eq!(function_type.results(), []); 86 | 87 | // Check that the module is well-formed by converting it to bytes and back. 88 | let module_bytes = module.emit_wasm(); 89 | Module::from_buffer(&module_bytes).unwrap(); 90 | } 91 | 92 | #[test] 93 | fn basic_module_with_no_table_export_and_drop_hook() { 94 | let module = wat::parse_file(simple_module_path()).unwrap(); 95 | let mut module = Module::from_buffer(&module).unwrap(); 96 | add_basic_custom_section(&mut module); 97 | 98 | Processor::default() 99 | .set_ref_table(None) 100 | .set_drop_fn("hook", "drop_ref") 101 | .process(&mut module) 102 | .unwrap(); 103 | 104 | // Check that the drop hook is imported. 105 | assert_eq!(module.imports.iter().count(), 2, "{:?}", module.imports); 106 | let import_id = module.imports.find("hook", "drop_ref").unwrap(); 107 | let import_id = match &module.imports.get(import_id).kind { 108 | ImportKind::Function(fn_id) => *fn_id, 109 | other => panic!("unexpected import type: {other:?}"), 110 | }; 111 | let function_type = module.types.get(module.funcs.get(import_id).ty()); 112 | assert_eq!(function_type.params(), [EXTERNREF]); 113 | assert_eq!(function_type.results(), []); 114 | 115 | // Check that the refs table is not exported. 116 | assert!(!module 117 | .exports 118 | .iter() 119 | .any(|export| matches!(export.item, ExportItem::Table(_)))); 120 | 121 | // Check that the module is well-formed by converting it to bytes and back. 122 | let module_bytes = module.emit_wasm(); 123 | Module::from_buffer(&module_bytes).unwrap(); 124 | } 125 | 126 | #[test] 127 | fn module_without_inlines() { 128 | let module = wat::parse_file(no_inline_module_path()).unwrap(); 129 | let mut module = Module::from_buffer(&module).unwrap(); 130 | // We need to add a custom section to the module before processing. 131 | add_basic_custom_section(&mut module); 132 | 133 | Processor::default().process(&mut module).unwrap(); 134 | 135 | // Check that the module is well-formed by converting it to bytes and back. 136 | let module_bytes = module.emit_wasm(); 137 | Module::from_buffer(&module_bytes).unwrap(); 138 | } 139 | -------------------------------------------------------------------------------- /crates/lib/tests/version_match.rs: -------------------------------------------------------------------------------- 1 | use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; 2 | 3 | #[test] 4 | fn readme_is_in_sync() { 5 | assert_markdown_deps_updated!("README.md"); 6 | } 7 | 8 | #[test] 9 | fn html_root_url_is_in_sync() { 10 | assert_html_root_url_updated!("src/lib.rs"); 11 | } 12 | -------------------------------------------------------------------------------- /crates/macro/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /crates/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externref-macro" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | keywords = ["externref", "anyref", "wasm"] 11 | categories = ["wasm", "development-tools::ffi"] 12 | description = "Proc macro for `externref`" 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | proc-macro2.workspace = true 22 | quote.workspace = true 23 | syn = { workspace = true, features = ["full"] } 24 | 25 | [dev-dependencies] 26 | syn = { workspace = true, features = ["extra-traits"] } 27 | trybuild.workspace = true 28 | version-sync.workspace = true 29 | -------------------------------------------------------------------------------- /crates/macro/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/macro/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /crates/macro/README.md: -------------------------------------------------------------------------------- 1 | # Proc Macro For `externref` 2 | 3 | [![Build Status](https://github.com/slowli/externref/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/externref/actions) 4 | [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/externref#license) 5 | ![rust 1.76+ required](https://img.shields.io/badge/rust-1.76+-blue.svg?label=Required%20Rust) 6 | 7 | **Documentation:** [![Docs.rs](https://docs.rs/externref-macro/badge.svg)](https://docs.rs/externref-macro/) 8 | [![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://slowli.github.io/externref/externref_macro/) 9 | 10 | This macro complements the [`externref`] library, wrapping imported or exported functions 11 | with `Resource` args and/or return type. These wrappers are what makes it possible to patch 12 | the generated WASM module with the `externref` processor, so that real `externref`s are used in 13 | argument / return type positions. 14 | 15 | ## Usage 16 | 17 | Add this to your `Crate.toml`: 18 | 19 | ```toml 20 | [dependencies] 21 | externref-macro = "0.3.0-beta.1" 22 | ``` 23 | 24 | Note that the `externref` crate re-exports the proc macro if the `macro` crate feature 25 | is enabled (which it is by default). Thus, it is rarely necessary to use this crate 26 | as a direct dependency. 27 | 28 | See `externref` docs for more details and examples of usage. 29 | 30 | ## License 31 | 32 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) 33 | or [MIT license](LICENSE-MIT) at your option. 34 | 35 | Unless you explicitly state otherwise, any contribution intentionally submitted 36 | for inclusion in `externref` by you, as defined in the Apache-2.0 license, 37 | shall be dual licensed as above, without any additional terms or conditions. 38 | 39 | [`externref`]: https://crates.io/crates/externref 40 | -------------------------------------------------------------------------------- /crates/macro/src/externref.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, mem}; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::{ 6 | parse::Error as SynError, punctuated::Punctuated, spanned::Spanned, Attribute, Expr, ExprLit, 7 | FnArg, ForeignItem, GenericArgument, Ident, ItemFn, ItemForeignMod, Lit, LitStr, Meta, PatType, 8 | Path, PathArguments, Signature, Token, Type, TypePath, Visibility, 9 | }; 10 | 11 | use crate::ExternrefAttrs; 12 | 13 | fn check_abi( 14 | target_name: &str, 15 | abi_name: Option<&LitStr>, 16 | root_span: &impl ToTokens, 17 | ) -> Result<(), SynError> { 18 | let abi_name = abi_name.ok_or_else(|| { 19 | let msg = format!("{target_name} must be marked with `extern \"C\"`"); 20 | SynError::new_spanned(root_span, msg) 21 | })?; 22 | if abi_name.value() != "C" { 23 | let msg = format!( 24 | "Unexpected ABI {} for {target_name}; expected `C`", 25 | abi_name.value() 26 | ); 27 | return Err(SynError::new(abi_name.span(), msg)); 28 | } 29 | Ok(()) 30 | } 31 | 32 | fn attr_expr(attrs: &[Attribute], name: &str) -> Result, SynError> { 33 | let attr = attrs.iter().find(|attr| attr.path().is_ident(name)); 34 | let Some(attr) = attr else { 35 | return Ok(None); 36 | }; 37 | 38 | let name_value = attr.meta.require_name_value()?; 39 | Ok(Some(name_value.value.clone())) 40 | } 41 | 42 | #[derive(Debug, Clone, Copy, PartialEq)] 43 | enum SimpleResourceKind { 44 | Owned, 45 | Ref, 46 | MutRef, 47 | } 48 | 49 | impl SimpleResourceKind { 50 | fn is_resource(ty: &TypePath) -> bool { 51 | ty.path.segments.last().is_some_and(|segment| { 52 | segment.ident == "Resource" 53 | && matches!( 54 | &segment.arguments, 55 | PathArguments::AngleBracketed(args) if args.args.len() == 1 56 | ) 57 | }) 58 | } 59 | 60 | fn from_type(ty: &Type) -> Option { 61 | match ty { 62 | Type::Path(path) if Self::is_resource(path) => Some(Self::Owned), 63 | Type::Reference(reference) => { 64 | if let Type::Path(path) = reference.elem.as_ref() { 65 | if Self::is_resource(path) { 66 | return Some(if reference.mutability.is_some() { 67 | Self::MutRef 68 | } else { 69 | Self::Ref 70 | }); 71 | } 72 | } 73 | None 74 | } 75 | _ => None, 76 | } 77 | } 78 | } 79 | 80 | #[derive(Debug, Clone, Copy, PartialEq)] 81 | enum ResourceKind { 82 | Simple(SimpleResourceKind), 83 | Option(SimpleResourceKind), 84 | } 85 | 86 | impl From for ResourceKind { 87 | fn from(simple: SimpleResourceKind) -> Self { 88 | Self::Simple(simple) 89 | } 90 | } 91 | 92 | impl ResourceKind { 93 | fn parse_option(ty: &TypePath) -> Option<&Type> { 94 | let segment = ty.path.segments.last()?; 95 | if segment.ident == "Option" { 96 | if let PathArguments::AngleBracketed(args) = &segment.arguments { 97 | if args.args.len() == 1 { 98 | if let GenericArgument::Type(ty) = args.args.first().unwrap() { 99 | return Some(ty); 100 | } 101 | } 102 | } 103 | } 104 | None 105 | } 106 | 107 | fn from_type(ty: &Type) -> Option { 108 | if let Some(kind) = SimpleResourceKind::from_type(ty) { 109 | return Some(kind.into()); 110 | } 111 | 112 | if let Type::Path(path) = ty { 113 | Self::parse_option(path) 114 | .and_then(|inner_ty| SimpleResourceKind::from_type(inner_ty).map(Self::Option)) 115 | } else { 116 | None 117 | } 118 | } 119 | 120 | fn simple_kind(self) -> SimpleResourceKind { 121 | match self { 122 | Self::Simple(simple) | Self::Option(simple) => simple, 123 | } 124 | } 125 | 126 | fn initialize_for_export(self, arg: &Ident, cr: &Path) -> TokenStream { 127 | match self { 128 | Self::Option(_) => { 129 | let method_call = match self.simple_kind() { 130 | SimpleResourceKind::Owned => None, 131 | SimpleResourceKind::Ref => Some(quote!(.as_ref())), 132 | SimpleResourceKind::MutRef => Some(quote!(.as_mut())), 133 | }; 134 | quote!(#cr::Resource::new(#arg) #method_call) 135 | } 136 | Self::Simple(_) => { 137 | let ref_token = match self.simple_kind() { 138 | SimpleResourceKind::Owned => None, 139 | SimpleResourceKind::Ref => Some(quote!(&)), 140 | SimpleResourceKind::MutRef => Some(quote!(&mut)), 141 | }; 142 | quote!(#ref_token #cr::Resource::new_non_null(#arg)) 143 | } 144 | } 145 | } 146 | 147 | fn prepare_for_import(self, arg: &Ident, cr: &Path) -> TokenStream { 148 | let arg = match self { 149 | Self::Simple(_) => quote!(core::option::Option::Some(#arg)), 150 | Self::Option(_) => quote!(#arg), 151 | }; 152 | 153 | match self.simple_kind() { 154 | SimpleResourceKind::Ref | SimpleResourceKind::MutRef => { 155 | quote!(#cr::Resource::raw(#arg)) 156 | } 157 | SimpleResourceKind::Owned => quote!(#cr::Resource::take_raw(#arg)), 158 | } 159 | } 160 | } 161 | 162 | #[derive(Debug, PartialEq)] 163 | enum ReturnType { 164 | Default, 165 | NotResource, 166 | Resource(ResourceKind), 167 | } 168 | 169 | struct Function { 170 | name: Expr, 171 | arg_count: usize, 172 | resource_args: HashMap, 173 | return_type: ReturnType, 174 | crate_path: Path, 175 | } 176 | 177 | impl Function { 178 | fn new(function: &ItemFn, attrs: &ExternrefAttrs) -> Result { 179 | let abi_name = function.sig.abi.as_ref().and_then(|abi| abi.name.as_ref()); 180 | check_abi("exported function", abi_name, &function.sig)?; 181 | 182 | if let Some(variadic) = &function.sig.variadic { 183 | let msg = "Variadic functions are not supported"; 184 | return Err(SynError::new_spanned(variadic, msg)); 185 | } 186 | let export_name = attr_expr(&function.attrs, "export_name")?; 187 | Ok(Self::from_sig(&function.sig, export_name, attrs)) 188 | } 189 | 190 | fn from_sig(sig: &Signature, name_override: Option, attrs: &ExternrefAttrs) -> Self { 191 | let resource_args = sig.inputs.iter().enumerate().filter_map(|(i, arg)| { 192 | if let FnArg::Typed(PatType { ty, .. }) = arg { 193 | return ResourceKind::from_type(ty).map(|kind| (i, kind)); 194 | } 195 | None 196 | }); 197 | let return_type = match &sig.output { 198 | syn::ReturnType::Type(_, ty) => { 199 | ResourceKind::from_type(ty).map_or(ReturnType::NotResource, ReturnType::Resource) 200 | } 201 | syn::ReturnType::Default => ReturnType::Default, 202 | }; 203 | let name = name_override.unwrap_or_else(|| { 204 | let str = sig.ident.to_string(); 205 | syn::parse_quote!(#str) 206 | }); 207 | 208 | Self { 209 | name, 210 | arg_count: sig.inputs.len(), 211 | resource_args: resource_args.collect(), 212 | return_type, 213 | crate_path: attrs.crate_path(), 214 | } 215 | } 216 | 217 | fn needs_declaring(&self) -> bool { 218 | !self.resource_args.is_empty() || matches!(self.return_type, ReturnType::Resource(_)) 219 | } 220 | 221 | fn declare(&self, module_name: Option<&str>) -> impl ToTokens { 222 | let name = &self.name; 223 | let cr = &self.crate_path; 224 | let kind = if let Some(module_name) = module_name { 225 | quote!(#cr::FunctionKind::Import(#module_name)) 226 | } else { 227 | quote!(#cr::FunctionKind::Export) 228 | }; 229 | let externrefs = self.create_externrefs(); 230 | 231 | quote! { 232 | #cr::declare_function!(#cr::Function { 233 | kind: #kind, 234 | name: #name, 235 | externrefs: #externrefs, 236 | }); 237 | } 238 | } 239 | 240 | fn wrap_export(&self, raw: &ItemFn, export_name: Option) -> impl ToTokens { 241 | let cr = &self.crate_path; 242 | let export_name = export_name.unwrap_or_else(|| { 243 | let name = raw.sig.ident.to_string(); 244 | syn::parse_quote!(#[export_name = #name]) 245 | }); 246 | let mut export_sig = raw.sig.clone(); 247 | export_sig.abi = Some(syn::parse_quote!(extern "C")); 248 | export_sig.unsafety = Some(syn::parse_quote!(unsafe)); 249 | export_sig.ident = Ident::new("__externref_export", export_sig.ident.span()); 250 | 251 | let mut args = Vec::with_capacity(export_sig.inputs.len()); 252 | for (i, arg) in export_sig.inputs.iter_mut().enumerate() { 253 | if let FnArg::Typed(typed_arg) = arg { 254 | let arg = Ident::new(&format!("__arg{i}"), typed_arg.pat.span()); 255 | typed_arg.pat = Box::new(syn::parse_quote!(#arg)); 256 | 257 | if let Some(kind) = self.resource_args.get(&i) { 258 | typed_arg.ty = Box::new(syn::parse_quote!(#cr::ExternRef)); 259 | args.push(kind.initialize_for_export(&arg, cr)); 260 | } else { 261 | args.push(quote!(#arg)); 262 | } 263 | } 264 | } 265 | 266 | let original_name = &raw.sig.ident; 267 | let delegation = quote!(#original_name(#(#args,)*)); 268 | let delegation = match self.return_type { 269 | ReturnType::Resource(kind) => { 270 | export_sig.output = syn::parse_quote!(-> #cr::ExternRef); 271 | let output = Ident::new("__output", raw.sig.span()); 272 | let conversion = kind.prepare_for_import(&output, cr); 273 | quote! { 274 | let #output = #delegation; 275 | #conversion 276 | } 277 | } 278 | ReturnType::NotResource => delegation, 279 | ReturnType::Default => quote!(#delegation;), 280 | }; 281 | 282 | quote! { 283 | const _: () = { 284 | #export_name 285 | #export_sig { 286 | #delegation 287 | } 288 | }; 289 | } 290 | } 291 | 292 | fn wrap_import(&self, vis: &Visibility, mut sig: Signature) -> (TokenStream, Ident) { 293 | let cr = &self.crate_path; 294 | sig.unsafety = Some(syn::parse_quote!(unsafe)); 295 | let new_ident = format!("__externref_{}", sig.ident); 296 | let new_ident = Ident::new(&new_ident, sig.ident.span()); 297 | 298 | let mut args = Vec::with_capacity(sig.inputs.len()); 299 | for (i, arg) in sig.inputs.iter_mut().enumerate() { 300 | if let FnArg::Typed(typed_arg) = arg { 301 | let arg = Ident::new(&format!("__arg{i}"), typed_arg.pat.span()); 302 | typed_arg.pat = Box::new(syn::parse_quote!(#arg)); 303 | 304 | if let Some(kind) = self.resource_args.get(&i) { 305 | args.push(kind.prepare_for_import(&arg, cr)); 306 | } else { 307 | args.push(quote!(#arg)); 308 | } 309 | } 310 | } 311 | 312 | let delegation = quote!(#new_ident(#(#args,)*)); 313 | let delegation = match self.return_type { 314 | ReturnType::Resource(kind) => { 315 | let output = Ident::new("__output", sig.span()); 316 | let init = kind.initialize_for_export(&output, cr); 317 | quote! { 318 | let #output = #delegation; 319 | #init 320 | } 321 | } 322 | ReturnType::NotResource => delegation, 323 | ReturnType::Default => quote!(#delegation;), 324 | }; 325 | 326 | let wrapper = quote! { 327 | #[inline(never)] 328 | #vis #sig { 329 | unsafe { #cr::ExternRef::guard(); } 330 | #delegation 331 | } 332 | }; 333 | (wrapper, new_ident) 334 | } 335 | 336 | fn create_externrefs(&self) -> impl ToTokens { 337 | let cr = &self.crate_path; 338 | let args_and_return_type_count = if matches!(self.return_type, ReturnType::Default) { 339 | self.arg_count 340 | } else { 341 | self.arg_count + 1 342 | }; 343 | let bytes = args_and_return_type_count.div_ceil(8); 344 | 345 | let maybe_ret_idx = if matches!(self.return_type, ReturnType::Resource(_)) { 346 | Some(self.arg_count) 347 | } else { 348 | None 349 | }; 350 | 351 | let set_bits = self.resource_args.keys().copied(); 352 | #[cfg(test)] // sort keys in deterministic order for testing 353 | let set_bits = { 354 | let mut sorted: Vec<_> = set_bits.collect(); 355 | sorted.sort_unstable(); 356 | sorted.into_iter() 357 | }; 358 | let set_bits = set_bits.chain(maybe_ret_idx); 359 | let set_bits = set_bits.map(|idx| quote!(.with_set_bit(#idx))); 360 | 361 | quote! { 362 | #cr::BitSlice::builder::<#bytes>(#args_and_return_type_count) 363 | #(#set_bits)* 364 | .build() 365 | } 366 | } 367 | } 368 | 369 | pub(crate) fn for_export(function: &mut ItemFn, attrs: &ExternrefAttrs) -> TokenStream { 370 | let parsed_function = match Function::new(function, attrs) { 371 | Ok(function) => function, 372 | Err(err) => return err.into_compile_error(), 373 | }; 374 | let (declaration, export) = if parsed_function.needs_declaring() { 375 | // "Un-export" the function by removing the relevant attributes. 376 | function.sig.abi = None; 377 | let attr_idx = function.attrs.iter().enumerate().find_map(|(idx, attr)| { 378 | if attr.path().is_ident("export_name") { 379 | Some(idx) 380 | } else { 381 | None 382 | } 383 | }); 384 | let export_name_attr = attr_idx.map(|idx| function.attrs.remove(idx)); 385 | 386 | // Remove `#[no_mangle]` attr if present as well; if it is retained, it will still 387 | // generate an export. 388 | function 389 | .attrs 390 | .retain(|attr| !attr.path().is_ident("no_mangle")); 391 | 392 | let export = parsed_function.wrap_export(function, export_name_attr); 393 | (Some(parsed_function.declare(None)), Some(export)) 394 | } else { 395 | (None, None) 396 | }; 397 | 398 | quote! { 399 | #function 400 | #export 401 | #declaration 402 | } 403 | } 404 | 405 | struct Imports { 406 | module_name: String, 407 | functions: Vec<(Function, TokenStream)>, 408 | } 409 | 410 | impl Imports { 411 | fn new(module: &mut ItemForeignMod, attrs: &ExternrefAttrs) -> Result { 412 | const NO_ATTR_MSG: &str = "#[link(wasm_import_module = \"..\")] must be specified \ 413 | on the foreign module"; 414 | 415 | check_abi("foreign module", module.abi.name.as_ref(), &module.abi)?; 416 | 417 | let link_attr = module 418 | .attrs 419 | .iter() 420 | .find(|attr| attr.path().is_ident("link")); 421 | let Some(link_attr) = link_attr else { 422 | return Err(SynError::new_spanned(module, NO_ATTR_MSG)); 423 | }; 424 | 425 | let module_name = if matches!(link_attr.meta, Meta::List(_)) { 426 | let nested = 427 | link_attr.parse_args_with(Punctuated::::parse_terminated)?; 428 | nested 429 | .into_iter() 430 | .find_map(|nested_meta| match nested_meta { 431 | Meta::NameValue(nv) if nv.path.is_ident("wasm_import_module") => Some(nv.value), 432 | _ => None, 433 | }) 434 | } else { 435 | let msg = 436 | "Unexpected contents of `#[link(..)]` attr (expected a list of name-value pairs)"; 437 | return Err(SynError::new_spanned(link_attr, msg)); 438 | }; 439 | 440 | let module_name = 441 | module_name.ok_or_else(|| SynError::new_spanned(link_attr, NO_ATTR_MSG))?; 442 | let module_name = if let Expr::Lit(ExprLit { 443 | lit: Lit::Str(str), .. 444 | }) = module_name 445 | { 446 | str.value() 447 | } else { 448 | let msg = "Unexpected WASM module name format (expected a string)"; 449 | return Err(SynError::new(module_name.span(), msg)); 450 | }; 451 | 452 | let cr = attrs.crate_path(); 453 | let mut functions = Vec::with_capacity(module.items.len()); 454 | for item in &mut module.items { 455 | if let ForeignItem::Fn(fn_item) = item { 456 | let link_name = attr_expr(&fn_item.attrs, "link_name")?; 457 | let has_link_name = link_name.is_some(); 458 | let function = Function::from_sig(&fn_item.sig, link_name, attrs); 459 | if !function.needs_declaring() { 460 | continue; 461 | } 462 | 463 | let vis = mem::replace(&mut fn_item.vis, Visibility::Inherited); 464 | let (wrapper, new_ident) = function.wrap_import(&vis, fn_item.sig.clone()); 465 | if !has_link_name { 466 | // Add `#[link_name = ".."]` since the function is renamed. 467 | let name = fn_item.sig.ident.to_string(); 468 | fn_item.attrs.push(syn::parse_quote!(#[link_name = #name])); 469 | } 470 | fn_item.sig.ident = new_ident; 471 | 472 | // Change function signature to use `usize`s in place of `Resource`s. 473 | for (i, arg) in fn_item.sig.inputs.iter_mut().enumerate() { 474 | if function.resource_args.contains_key(&i) { 475 | if let FnArg::Typed(typed_arg) = arg { 476 | typed_arg.ty = Box::new(syn::parse_quote!(#cr::ExternRef)); 477 | } 478 | } 479 | } 480 | if matches!(function.return_type, ReturnType::Resource(_)) { 481 | fn_item.sig.output = syn::parse_quote!(-> #cr::ExternRef); 482 | } 483 | 484 | functions.push((function, wrapper)); 485 | } 486 | } 487 | 488 | Ok(Self { 489 | module_name, 490 | functions, 491 | }) 492 | } 493 | 494 | fn declarations(&self) -> impl ToTokens { 495 | let function_declarations = self 496 | .functions 497 | .iter() 498 | .map(|(function, _)| function.declare(Some(&self.module_name))); 499 | quote!(#(#function_declarations)*) 500 | } 501 | 502 | fn wrappers(&self) -> impl ToTokens { 503 | let wrappers = self.functions.iter().map(|(_, wrapper)| wrapper); 504 | quote!(#(#wrappers)*) 505 | } 506 | } 507 | 508 | pub(crate) fn for_foreign_module( 509 | module: &mut ItemForeignMod, 510 | attrs: &ExternrefAttrs, 511 | ) -> TokenStream { 512 | let parsed_module = match Imports::new(module, attrs) { 513 | Ok(module) => module, 514 | Err(err) => return err.into_compile_error(), 515 | }; 516 | let declarations = parsed_module.declarations(); 517 | let wrappers = parsed_module.wrappers(); 518 | quote! { 519 | #module 520 | #declarations 521 | #wrappers 522 | } 523 | } 524 | 525 | #[cfg(test)] 526 | mod tests { 527 | use super::*; 528 | 529 | #[test] 530 | fn declaring_signature_for_export() { 531 | let export_fn: ItemFn = syn::parse_quote! { 532 | pub extern "C" fn test_export( 533 | sender: &mut Resource, 534 | buffer: Resource, 535 | some_ptr: *const u8, 536 | ) { 537 | // does nothing 538 | } 539 | }; 540 | let parsed = Function::new(&export_fn, &ExternrefAttrs::default()).unwrap(); 541 | assert!(parsed.needs_declaring()); 542 | 543 | let declaration = parsed.declare(None); 544 | let declaration: syn::Item = syn::parse_quote!(#declaration); 545 | let expected: syn::Item = syn::parse_quote! { 546 | externref::declare_function!(externref::Function { 547 | kind: externref::FunctionKind::Export, 548 | name: "test_export", 549 | externrefs: externref::BitSlice::builder::<1usize>(3usize) 550 | .with_set_bit(0usize) 551 | .with_set_bit(1usize) 552 | .build(), 553 | }); 554 | }; 555 | assert_eq!(declaration, expected, "{}", quote!(#declaration)); 556 | } 557 | 558 | #[test] 559 | fn transforming_export() { 560 | let export_fn: ItemFn = syn::parse_quote! { 561 | pub extern "C" fn test_export( 562 | sender: &mut Resource, 563 | buffer: Option>, 564 | some_ptr: *const u8, 565 | ) { 566 | // does nothing 567 | } 568 | }; 569 | let parsed = Function::new(&export_fn, &ExternrefAttrs::default()).unwrap(); 570 | assert_eq!(parsed.resource_args.len(), 2); 571 | assert_eq!(parsed.resource_args[&0], SimpleResourceKind::MutRef.into()); 572 | assert_eq!( 573 | parsed.resource_args[&1], 574 | ResourceKind::Option(SimpleResourceKind::Owned) 575 | ); 576 | assert_eq!(parsed.return_type, ReturnType::Default); 577 | 578 | let wrapper = parsed.wrap_export(&export_fn, None); 579 | let wrapper: syn::Item = syn::parse_quote!(#wrapper); 580 | let expected: syn::Item = syn::parse_quote! { 581 | const _: () = { 582 | #[export_name = "test_export"] 583 | unsafe extern "C" fn __externref_export( 584 | __arg0: externref::ExternRef, 585 | __arg1: externref::ExternRef, 586 | __arg2: *const u8, 587 | ) { 588 | test_export( 589 | &mut externref::Resource::new_non_null(__arg0), 590 | externref::Resource::new(__arg1), 591 | __arg2, 592 | ); 593 | } 594 | }; 595 | }; 596 | assert_eq!(wrapper, expected, "{}", quote!(#wrapper)); 597 | } 598 | 599 | #[test] 600 | fn wrapper_for_import() { 601 | let sig: Signature = syn::parse_quote! { 602 | fn send_message( 603 | sender: &Resource, 604 | message_ptr: *const u8, 605 | message_len: usize, 606 | ) -> Resource 607 | }; 608 | let parsed = Function::from_sig(&sig, None, &ExternrefAttrs::default()); 609 | 610 | let (wrapper, ident) = parsed.wrap_import(&Visibility::Inherited, sig); 611 | assert_eq!(ident, "__externref_send_message"); 612 | 613 | let wrapper: ItemFn = syn::parse_quote!(#wrapper); 614 | let expected: ItemFn = syn::parse_quote! { 615 | #[inline(never)] 616 | unsafe fn send_message( 617 | __arg0: &Resource, 618 | __arg1: *const u8, 619 | __arg2: usize, 620 | ) -> Resource { 621 | unsafe { externref::ExternRef::guard(); } 622 | let __output = __externref_send_message( 623 | externref::Resource::raw(core::option::Option::Some(__arg0)), 624 | __arg1, 625 | __arg2, 626 | ); 627 | externref::Resource::new_non_null(__output) 628 | } 629 | }; 630 | assert_eq!(wrapper, expected, "{}", quote!(#wrapper)); 631 | } 632 | 633 | #[test] 634 | fn foreign_mod_transformation() { 635 | let mut foreign_mod: ItemForeignMod = syn::parse_quote! { 636 | #[link(wasm_import_module = "test")] 637 | extern "C" { 638 | fn send_message( 639 | sender: &Resource, 640 | message_ptr: *const u8, 641 | message_len: usize, 642 | ) -> Resource; 643 | } 644 | }; 645 | Imports::new(&mut foreign_mod, &ExternrefAttrs::default()).unwrap(); 646 | 647 | let expected: ItemForeignMod = syn::parse_quote! { 648 | #[link(wasm_import_module = "test")] 649 | extern "C" { 650 | #[link_name = "send_message"] 651 | fn __externref_send_message( 652 | sender: externref::ExternRef, 653 | message_ptr: *const u8, 654 | message_len: usize, 655 | ) -> externref::ExternRef; 656 | } 657 | }; 658 | assert_eq!(foreign_mod, expected, "{}", quote!(#foreign_mod)); 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /crates/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macro for [`externref`]. 2 | //! 3 | //! This macro wraps imported or exported functions with `Resource` args / return type 4 | //! doing all heavy lifting to prepare these functions for usage with `externref`s. 5 | //! Note that it is necessary to post-process the module with the module processor provided 6 | //! by the `externref` crate. 7 | //! 8 | //! See `externref` docs for more details and examples of usage. 9 | //! 10 | //! [`externref`]: https://docs.rs/externref 11 | 12 | #![recursion_limit = "128"] 13 | // Documentation settings. 14 | #![doc(html_root_url = "https://docs.rs/externref-macro/0.3.0-beta.1")] 15 | // Linter settings. 16 | #![warn(missing_debug_implementations, missing_docs, bare_trait_objects)] 17 | #![warn(clippy::all, clippy::pedantic)] 18 | #![allow(clippy::must_use_candidate, clippy::module_name_repetitions)] 19 | 20 | extern crate proc_macro; 21 | 22 | use proc_macro::TokenStream; 23 | use syn::{ 24 | parse::{Error as SynError, Parser}, 25 | Item, Path, 26 | }; 27 | 28 | mod externref; 29 | 30 | use crate::externref::{for_export, for_foreign_module}; 31 | 32 | #[derive(Default)] 33 | struct ExternrefAttrs { 34 | crate_path: Option, 35 | } 36 | 37 | impl ExternrefAttrs { 38 | fn parse(tokens: TokenStream) -> syn::Result { 39 | let mut attrs = Self::default(); 40 | if tokens.is_empty() { 41 | return Ok(attrs); 42 | } 43 | 44 | let parser = syn::meta::parser(|meta| { 45 | if meta.path.is_ident("crate") { 46 | let value = meta.value()?; 47 | attrs.crate_path = Some(if let Ok(path_str) = value.parse::() { 48 | path_str.parse()? 49 | } else { 50 | value.parse()? 51 | }); 52 | Ok(()) 53 | } else { 54 | Err(meta.error("unsupported attribute")) 55 | } 56 | }); 57 | parser.parse(tokens)?; 58 | Ok(attrs) 59 | } 60 | 61 | fn crate_path(&self) -> Path { 62 | self.crate_path 63 | .clone() 64 | .unwrap_or_else(|| syn::parse_quote!(externref)) 65 | } 66 | } 67 | 68 | /// Prepares imported functions or an exported function with `Resource` args and/or return type. 69 | /// 70 | /// # Inputs 71 | /// 72 | /// This attribute must be placed on an `extern "C" { ... }` block or an `extern "C" fn`. 73 | /// If placed on block, all enclosed functions with `Resource` args / return type will be 74 | /// wrapped. 75 | /// 76 | /// # Processing 77 | /// 78 | /// The following arg / return types are recognized as resources: 79 | /// 80 | /// - `Resource<_>`, `&Resource<_>`, `&mut Resource<_>` 81 | /// - `Option<_>` of any of the above three variations 82 | #[proc_macro_attribute] 83 | pub fn externref(attr: TokenStream, input: TokenStream) -> TokenStream { 84 | const MSG: &str = "Unsupported item; only `extern \"C\" {}` modules and `extern \"C\" fn ...` \ 85 | exports are supported"; 86 | 87 | let attrs = match ExternrefAttrs::parse(attr) { 88 | Ok(attrs) => attrs, 89 | Err(err) => return err.into_compile_error().into(), 90 | }; 91 | 92 | let output = match syn::parse::(input) { 93 | Ok(Item::ForeignMod(mut module)) => for_foreign_module(&mut module, &attrs), 94 | Ok(Item::Fn(mut function)) => for_export(&mut function, &attrs), 95 | Ok(other) => { 96 | return SynError::new_spanned(other, MSG) 97 | .into_compile_error() 98 | .into() 99 | } 100 | Err(err) => return err.into_compile_error().into(), 101 | }; 102 | output.into() 103 | } 104 | -------------------------------------------------------------------------------- /crates/macro/tests/integration.rs: -------------------------------------------------------------------------------- 1 | //! UI tests for various compilation failures. 2 | 3 | #[test] 4 | fn ui() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/ui/*.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/fn_with_bogus_export_name.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | #[export_name("what")] 5 | pub extern "C" fn bogus() { 6 | // Do nothing 7 | } 8 | 9 | #[externref] 10 | #[export_name = 10] 11 | pub extern "C" fn bogus_too() { 12 | // Do nothing 13 | } 14 | 15 | fn main() {} 16 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/fn_with_bogus_export_name.stderr: -------------------------------------------------------------------------------- 1 | error: expected `=` 2 | --> tests/ui/fn_with_bogus_export_name.rs:4:14 3 | | 4 | 4 | #[export_name("what")] 5 | | ^ 6 | 7 | error: malformed `export_name` attribute input 8 | --> tests/ui/fn_with_bogus_export_name.rs:10:1 9 | | 10 | 10 | #[export_name = 10] 11 | | ^^^^^^^^^^^^^^^^^^^ help: must be of the form: `#[export_name = "name"]` 12 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/item_with_bogus_abi.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | pub extern "win64" fn test() { 5 | // Does nothing. 6 | } 7 | 8 | #[externref] 9 | extern "win64" { 10 | pub fn unused(ptr: *const u8, len: usize); 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/item_with_bogus_abi.stderr: -------------------------------------------------------------------------------- 1 | error: Unexpected ABI win64 for exported function; expected `C` 2 | --> tests/ui/item_with_bogus_abi.rs:4:12 3 | | 4 | 4 | pub extern "win64" fn test() { 5 | | ^^^^^^^ 6 | 7 | error: Unexpected ABI win64 for foreign module; expected `C` 8 | --> tests/ui/item_with_bogus_abi.rs:9:8 9 | | 10 | 9 | extern "win64" { 11 | | ^^^^^^^ 12 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/item_without_abi.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | pub fn test() { 5 | // Does nothing. 6 | } 7 | 8 | #[externref] 9 | extern { 10 | pub fn unused(ptr: *const u8, len: usize); 11 | } 12 | 13 | fn main() {} 14 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/item_without_abi.stderr: -------------------------------------------------------------------------------- 1 | error: exported function must be marked with `extern "C"` 2 | --> tests/ui/item_without_abi.rs:4:5 3 | | 4 | 4 | pub fn test() { 5 | | ^^^^^^^^^ 6 | 7 | error: foreign module must be marked with `extern "C"` 8 | --> tests/ui/item_without_abi.rs:9:1 9 | | 10 | 9 | extern { 11 | | ^^^^^^ 12 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_with_bogus_link_name.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | #[link(wasm_import_module = "test")] 5 | extern "C" { 6 | #[link_name("huh")] 7 | pub fn unused(ptr: *const u8, len: usize); 8 | } 9 | 10 | #[externref] 11 | #[link(wasm_import_module = "test")] 12 | extern "C" { 13 | #[link_name = 3] 14 | pub fn other(ptr: *const u8, len: usize); 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_with_bogus_link_name.stderr: -------------------------------------------------------------------------------- 1 | error: expected `=` 2 | --> tests/ui/module_with_bogus_link_name.rs:6:16 3 | | 4 | 6 | #[link_name("huh")] 5 | | ^ 6 | 7 | error: malformed `link_name` attribute input 8 | --> tests/ui/module_with_bogus_link_name.rs:13:5 9 | | 10 | 13 | #[link_name = 3] 11 | | ^^^^^^^^^^^^^^^^ help: must be of the form: `#[link_name = "name"]` 12 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_with_bogus_name.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | #[link = 5] 5 | extern "C" { 6 | pub fn unused(ptr: *const u8, len: usize); 7 | } 8 | 9 | #[externref] 10 | #[link(wasm_module = "what")] 11 | extern "C" { 12 | pub fn unused(ptr: *const u8, len: usize); 13 | } 14 | 15 | #[externref] 16 | #[link(wasm_import_module = 5)] 17 | extern "C" { 18 | pub fn unused(ptr: *const u8, len: usize); 19 | } 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_with_bogus_name.stderr: -------------------------------------------------------------------------------- 1 | error: Unexpected contents of `#[link(..)]` attr (expected a list of name-value pairs) 2 | --> tests/ui/module_with_bogus_name.rs:4:1 3 | | 4 | 4 | #[link = 5] 5 | | ^^^^^^^^^^^ 6 | 7 | error: #[link(wasm_import_module = "..")] must be specified on the foreign module 8 | --> tests/ui/module_with_bogus_name.rs:10:1 9 | | 10 | 10 | #[link(wasm_module = "what")] 11 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | error: Unexpected WASM module name format (expected a string) 14 | --> tests/ui/module_with_bogus_name.rs:16:29 15 | | 16 | 16 | #[link(wasm_import_module = 5)] 17 | | ^ 18 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_without_name.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | extern "C" { 5 | pub fn unused(ptr: *const u8, len: usize); 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/module_without_name.stderr: -------------------------------------------------------------------------------- 1 | error: #[link(wasm_import_module = "..")] must be specified on the foreign module 2 | --> tests/ui/module_without_name.rs:4:1 3 | | 4 | 4 | / extern "C" { 5 | 5 | | pub fn unused(ptr: *const u8, len: usize); 6 | 6 | | } 7 | | |_^ 8 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/unsupported_item.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | pub struct Test; 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/unsupported_item.stderr: -------------------------------------------------------------------------------- 1 | error: Unsupported item; only `extern "C" {}` modules and `extern "C" fn ...` exports are supported 2 | --> tests/ui/unsupported_item.rs:4:1 3 | | 4 | 4 | pub struct Test; 5 | | ^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/variadic_fn.rs: -------------------------------------------------------------------------------- 1 | use externref_macro::externref; 2 | 3 | #[externref] 4 | pub extern "C" fn printf(format: *const c_char, ...) { 5 | // Do nothing 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /crates/macro/tests/ui/variadic_fn.stderr: -------------------------------------------------------------------------------- 1 | error: Variadic functions are not supported 2 | --> tests/ui/variadic_fn.rs:4:49 3 | | 4 | 4 | pub extern "C" fn printf(format: *const c_char, ...) { 5 | | ^^^ 6 | -------------------------------------------------------------------------------- /crates/macro/tests/version_match.rs: -------------------------------------------------------------------------------- 1 | use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; 2 | 3 | #[test] 4 | fn readme_is_in_sync() { 5 | assert_markdown_deps_updated!("README.md"); 6 | } 7 | 8 | #[test] 9 | fn html_root_url_is_in_sync() { 10 | assert_html_root_url_updated!("src/lib.rs"); 11 | } 12 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # `cargo-deny` configuration. 2 | 3 | [output] 4 | feature-depth = 1 5 | 6 | [advisories] 7 | db-urls = ["https://github.com/rustsec/advisory-db"] 8 | yanked = "deny" 9 | ignore = [ 10 | "RUSTSEC-2017-0008", # `serial` crate is unmaintained; depended on from test deps only 11 | "RUSTSEC-2024-0436", # paste - no longer maintained; depended on from test deps only 12 | ] 13 | 14 | [licenses] 15 | allow = [ 16 | # Permissive open-source licenses 17 | "MIT", 18 | "Apache-2.0", 19 | "Apache-2.0 WITH LLVM-exception", 20 | "Unicode-3.0", 21 | "Zlib", 22 | ] 23 | confidence-threshold = 0.8 24 | 25 | [bans] 26 | multiple-versions = "deny" 27 | wildcards = "deny" 28 | allow-wildcard-paths = true 29 | skip-tree = [ 30 | # Relied upon by `tracing` (via `regex`); no fix is possible ATM 31 | { name = "regex-automata", version = "^0.1" }, 32 | # Relied upon by multiple crates; since it largely provides OS bindings, duplication is sort of fine 33 | { name = "windows-sys", version = "^0.52" }, 34 | # Relied upon by `walrus` 35 | { name = "indexmap", version = "^1" }, 36 | { name = "hashbrown", version = "^0.14" }, 37 | ] 38 | 39 | [sources] 40 | unknown-registry = "deny" 41 | unknown-git = "deny" 42 | -------------------------------------------------------------------------------- /e2e-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externref-test" 3 | version = "0.0.0" 4 | edition.workspace = true 5 | license.workspace = true 6 | publish = false 7 | description = "End-to-end test crate for `externref`" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | externref = { workspace = true, features = ["default"] } 14 | 15 | [target.'cfg(target_arch = "wasm32")'.dependencies] 16 | dlmalloc = { workspace = true, features = ["global"] } 17 | 18 | [dev-dependencies] 19 | anyhow.workspace = true 20 | assert_matches.workspace = true 21 | once_cell.workspace = true 22 | predicates.workspace = true 23 | test-casing.workspace = true 24 | tracing.workspace = true 25 | tracing-capture.workspace = true 26 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 27 | wasmtime.workspace = true 28 | 29 | [dev-dependencies.externref] 30 | path = "../crates/lib" 31 | features = ["processor", "tracing"] 32 | -------------------------------------------------------------------------------- /e2e-tests/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests For `externref` 2 | 3 | This crate provides end-to-end tests for the `externref` crate. 4 | Testing works by defining a WASM module that uses `externref::Resource`s, 5 | processing this module with `externref::processor`, and then running 6 | this module using [`wasmtime`]. 7 | 8 | [`wasmtime`]: https://crates.io/crates/wasmtime 9 | -------------------------------------------------------------------------------- /e2e-tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! E2E test for `externref`. 2 | 3 | #![cfg_attr(target_arch = "wasm32", no_std)] 4 | 5 | extern crate alloc; 6 | 7 | use alloc::vec::Vec; 8 | 9 | use externref::{externref, Resource}; 10 | 11 | #[cfg(target_arch = "wasm32")] 12 | #[global_allocator] 13 | static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; 14 | 15 | #[cfg(target_arch = "wasm32")] 16 | #[panic_handler] 17 | fn handle_panic(_info: &core::panic::PanicInfo) -> ! { 18 | loop {} 19 | } 20 | 21 | pub struct Sender(()); 22 | 23 | pub struct Bytes(()); 24 | 25 | // Emulate reexporting the crate. 26 | mod reexports { 27 | pub use externref as anyref; 28 | } 29 | 30 | mod imports { 31 | use externref::Resource; 32 | 33 | use crate::{Bytes, Sender}; 34 | 35 | #[cfg(target_arch = "wasm32")] 36 | #[externref::externref] 37 | #[link(wasm_import_module = "test")] 38 | extern "C" { 39 | pub(crate) fn send_message( 40 | sender: &Resource, 41 | message_ptr: *const u8, 42 | message_len: usize, 43 | ) -> Resource; 44 | 45 | pub(crate) fn message_len(bytes: Option<&Resource>) -> usize; 46 | 47 | #[link_name = "inspect_refs"] 48 | pub(crate) fn inspect_refs_on_host(); 49 | } 50 | 51 | #[cfg(not(target_arch = "wasm32"))] 52 | pub(crate) unsafe fn send_message( 53 | _: &Resource, 54 | _: *const u8, 55 | _: usize, 56 | ) -> Resource { 57 | panic!("only callable from WASM") 58 | } 59 | 60 | #[cfg(not(target_arch = "wasm32"))] 61 | pub(crate) unsafe fn message_len(_: Option<&Resource>) -> usize { 62 | panic!("only callable from WASM") 63 | } 64 | } 65 | 66 | /// Calls to the host to check the `externrefs` table. 67 | fn inspect_refs() { 68 | #[cfg(target_arch = "wasm32")] 69 | unsafe { 70 | imports::inspect_refs_on_host(); 71 | } 72 | } 73 | 74 | #[externref] 75 | pub extern "C" fn test_export(sender: Resource) { 76 | let messages = ["test", "42", "some other string"] 77 | .into_iter() 78 | .map(|message| { 79 | inspect_refs(); 80 | unsafe { imports::send_message(&sender, message.as_ptr(), message.len()) } 81 | }); 82 | let mut messages: Vec<_> = messages.collect(); 83 | inspect_refs(); 84 | messages.swap_remove(0); 85 | inspect_refs(); 86 | drop(messages); 87 | inspect_refs(); 88 | } 89 | 90 | #[export_name = concat!("test_export_", stringify!(with_casts))] 91 | // ^ tests manually specified name with a complex expression 92 | #[externref] 93 | pub extern "C" fn test_export_with_casts(sender: Resource<()>) { 94 | let sender = unsafe { sender.downcast_unchecked() }; 95 | let messages = ["test", "42", "some other string"] 96 | .into_iter() 97 | .map(|message| { 98 | inspect_refs(); 99 | unsafe { imports::send_message(&sender, message.as_ptr(), message.len()) }.upcast() 100 | }); 101 | let mut messages: Vec<_> = messages.collect(); 102 | inspect_refs(); 103 | messages.swap_remove(0); 104 | inspect_refs(); 105 | drop(messages); 106 | inspect_refs(); 107 | } 108 | 109 | #[externref(crate = "crate::reexports::anyref")] 110 | pub extern "C" fn test_nulls(sender: Option<&Resource>) { 111 | let message = "test"; 112 | if let Some(sender) = sender { 113 | let bytes = unsafe { imports::send_message(sender, message.as_ptr(), message.len()) }; 114 | assert_eq!(unsafe { imports::message_len(Some(&bytes)) }, 4); 115 | } 116 | assert_eq!(unsafe { imports::message_len(None) }, 0); 117 | } 118 | 119 | // Add another param to the function so that it's not deduped with `test_nulls` 120 | // (incl. by `wasm-opt` over which we don't have any control). 121 | #[externref(crate = crate::reexports::anyref)] 122 | pub extern "C" fn test_nulls2(sender: Option<&Resource>, _unused: u32) { 123 | test_nulls(sender); 124 | } 125 | -------------------------------------------------------------------------------- /e2e-tests/tests/integration/compile.rs: -------------------------------------------------------------------------------- 1 | //! WASM module compilation logic. 2 | 3 | use std::{ 4 | env, fs, 5 | path::{Path, PathBuf}, 6 | process::{Command, Stdio}, 7 | }; 8 | 9 | fn target_dir() -> PathBuf { 10 | let mut path = env::current_exe().expect("Cannot get path to executing test"); 11 | path.pop(); 12 | if path.ends_with("deps") { 13 | path.pop(); 14 | } 15 | path 16 | } 17 | 18 | fn wasm_target_dir(target_dir: PathBuf, profile: &str) -> PathBuf { 19 | let mut root_dir = target_dir; 20 | while !root_dir.join("wasm32-unknown-unknown").is_dir() { 21 | assert!( 22 | root_dir.pop(), 23 | "Cannot find dir for the `wasm32-unknown-unknown` target" 24 | ); 25 | } 26 | root_dir.join("wasm32-unknown-unknown").join(profile) 27 | } 28 | 29 | fn compile_wasm(profile: &str) -> PathBuf { 30 | let profile_arg = if profile == "debug" { 31 | "--profile=dev".to_owned() // the debug profile has differing `--profile` and output dir naming 32 | } else { 33 | format!("--profile={profile}") 34 | }; 35 | let mut command = Command::new("cargo"); 36 | command.args([ 37 | "build", 38 | "--lib", 39 | "--target", 40 | "wasm32-unknown-unknown", 41 | &profile_arg, 42 | ]); 43 | 44 | let mut command = command 45 | .stdin(Stdio::null()) 46 | .spawn() 47 | .expect("cannot run cargo"); 48 | let exit_status = command.wait().expect("failed waiting for cargo"); 49 | assert!( 50 | exit_status.success(), 51 | "Compiling WASM module finished abnormally: {exit_status}" 52 | ); 53 | 54 | let wasm_dir = wasm_target_dir(target_dir(), profile); 55 | let mut wasm_file = env!("CARGO_PKG_NAME").replace('-', "_"); 56 | wasm_file.push_str(".wasm"); 57 | wasm_dir.join(wasm_file) 58 | } 59 | 60 | fn optimize_wasm(wasm_file: &Path) -> PathBuf { 61 | let mut opt_wasm_file = PathBuf::from(wasm_file); 62 | opt_wasm_file.set_extension("opt.wasm"); 63 | 64 | let mut command = Command::new("wasm-opt") 65 | .args(["-Os", "--enable-mutable-globals", "--strip-debug"]) 66 | .arg("-o") 67 | .args([opt_wasm_file.as_ref(), wasm_file]) 68 | .stdin(Stdio::null()) 69 | .spawn() 70 | .expect("cannot run wasm-opt"); 71 | 72 | let exit_status = command.wait().expect("failed waiting for wasm-opt"); 73 | assert!( 74 | exit_status.success(), 75 | "Optimizing WASM module finished abnormally: {exit_status}" 76 | ); 77 | opt_wasm_file 78 | } 79 | 80 | #[derive(Debug, Clone, Copy)] 81 | pub(crate) enum CompilationProfile { 82 | Wasm, 83 | OptimizedWasm, 84 | Debug, 85 | Release, 86 | } 87 | 88 | impl CompilationProfile { 89 | pub const ALL: [Self; 4] = [Self::Wasm, Self::OptimizedWasm, Self::Debug, Self::Release]; 90 | 91 | fn rust_profile(self) -> &'static str { 92 | match self { 93 | Self::Wasm | Self::OptimizedWasm => "wasm", 94 | Self::Debug => "debug", 95 | Self::Release => "release", 96 | } 97 | } 98 | 99 | pub fn compile(self) -> Vec { 100 | let mut wasm_file = compile_wasm(self.rust_profile()); 101 | if matches!(self, Self::OptimizedWasm) { 102 | wasm_file = optimize_wasm(&wasm_file); 103 | } 104 | fs::read(&wasm_file).unwrap_or_else(|err| { 105 | panic!( 106 | "Error reading file `{}`: {err}", 107 | wasm_file.to_string_lossy() 108 | ) 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /e2e-tests/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | //! End-to-end tests for the `externref` macro / processor. 2 | 3 | use std::{collections::HashSet, sync::Once}; 4 | 5 | use anyhow::{anyhow, Context}; 6 | use assert_matches::assert_matches; 7 | use externref::processor::Processor; 8 | use once_cell::sync::Lazy; 9 | use test_casing::{test_casing, Product}; 10 | use tracing::{subscriber::DefaultGuard, Level, Subscriber}; 11 | use tracing_capture::{CaptureLayer, SharedStorage, Storage}; 12 | use tracing_subscriber::{ 13 | fmt::format::FmtSpan, layer::SubscriberExt, registry::LookupSpan, FmtSubscriber, 14 | }; 15 | use wasmtime::{ 16 | Caller, Engine, Extern, ExternRef, Linker, ManuallyRooted, Module, Ref, Rooted, Store, Table, 17 | }; 18 | 19 | use crate::compile::CompilationProfile; 20 | 21 | mod compile; 22 | 23 | type RefAssertion = fn(Caller<'_, Data>, &Table); 24 | 25 | fn module_bytes(profile: CompilationProfile) -> &'static [u8] { 26 | static UNOPTIMIZED_MODULE: Lazy> = Lazy::new(|| CompilationProfile::Wasm.compile()); 27 | static OPTIMIZED_MODULE: Lazy> = 28 | Lazy::new(|| CompilationProfile::OptimizedWasm.compile()); 29 | static DEBUG_MODULE: Lazy> = Lazy::new(|| CompilationProfile::Debug.compile()); 30 | static RELEASE_MODULE: Lazy> = Lazy::new(|| CompilationProfile::Release.compile()); 31 | 32 | match profile { 33 | CompilationProfile::Wasm => &UNOPTIMIZED_MODULE, 34 | CompilationProfile::OptimizedWasm => &OPTIMIZED_MODULE, 35 | CompilationProfile::Debug => &DEBUG_MODULE, 36 | CompilationProfile::Release => &RELEASE_MODULE, 37 | } 38 | } 39 | 40 | fn create_fmt_subscriber() -> impl Subscriber + for<'a> LookupSpan<'a> { 41 | FmtSubscriber::builder() 42 | .pretty() 43 | .with_span_events(FmtSpan::CLOSE) 44 | .with_test_writer() 45 | .with_env_filter("externref=debug") 46 | .finish() 47 | } 48 | 49 | fn enable_tracing() { 50 | static TRACING: Once = Once::new(); 51 | 52 | TRACING.call_once(|| { 53 | tracing::subscriber::set_global_default(create_fmt_subscriber()).ok(); 54 | }); 55 | } 56 | 57 | fn enable_tracing_assertions() -> (DefaultGuard, SharedStorage) { 58 | let storage = SharedStorage::default(); 59 | let subscriber = create_fmt_subscriber().with(CaptureLayer::new(&storage)); 60 | let guard = tracing::subscriber::set_default(subscriber); 61 | (guard, storage) 62 | } 63 | 64 | #[derive(Debug)] 65 | struct HostSender { 66 | key: String, 67 | } 68 | 69 | struct Data { 70 | externrefs: Option, 71 | ref_assertions: Vec, 72 | senders: HashSet, 73 | dropped: Vec>, 74 | } 75 | 76 | impl Data { 77 | fn new(mut ref_assertions: Vec, &Table)>) -> Self { 78 | ref_assertions.reverse(); 79 | Self { 80 | externrefs: None, 81 | ref_assertions, 82 | senders: HashSet::new(), 83 | dropped: vec![], 84 | } 85 | } 86 | 87 | fn push_sender(&mut self, name: impl Into) -> HostSender { 88 | let name = name.into(); 89 | self.senders.insert(name.clone()); 90 | HostSender { key: name } 91 | } 92 | 93 | fn assert_drops(&self, store: &Store, expected_strings: &[&str]) { 94 | let dropped_strings = self.dropped.iter().filter_map(|drop| { 95 | drop.data(store) 96 | .expect("reference was unexpectedly garbage-collected") 97 | .unwrap() 98 | .downcast_ref::>() 99 | .map(AsRef::as_ref) 100 | }); 101 | let dropped_strings: Vec<&str> = dropped_strings.collect(); 102 | assert_eq!(dropped_strings, *expected_strings); 103 | } 104 | } 105 | 106 | fn send_message( 107 | mut ctx: Caller<'_, Data>, 108 | resource: Option>, 109 | buffer_ptr: u32, 110 | buffer_len: u32, 111 | ) -> anyhow::Result>> { 112 | let memory = ctx 113 | .get_export("memory") 114 | .and_then(Extern::into_memory) 115 | .ok_or_else(|| anyhow!("module memory is not exposed"))?; 116 | 117 | let mut buffer = vec![0_u8; buffer_len as usize]; 118 | memory 119 | .read(&ctx, buffer_ptr as usize, &mut buffer) 120 | .context("failed reading WASM memory")?; 121 | let buffer = String::from_utf8(buffer).context("buffer is not utf-8")?; 122 | 123 | let sender = resource 124 | .context("null reference passed to host")? 125 | .data(&ctx)? 126 | .context("null reference")? 127 | .downcast_ref::() 128 | .ok_or_else(|| anyhow!("passed reference has incorrect type"))?; 129 | assert!(ctx.data().senders.contains(&sender.key)); 130 | 131 | let bytes = Box::::from(buffer); 132 | ExternRef::new(&mut ctx, bytes).map(Some) 133 | } 134 | 135 | fn message_len(ctx: Caller<'_, Data>, resource: Option>) -> anyhow::Result { 136 | let Some(resource) = resource else { 137 | return Ok(0); 138 | }; 139 | let str = resource 140 | .data(&ctx) 141 | .context("passed reference is garbage-collected")? 142 | .context("null reference")? 143 | .downcast_ref::>() 144 | .context("passed reference has incorrect type")?; 145 | Ok(u32::try_from(str.len()).unwrap()) 146 | } 147 | 148 | fn inspect_refs(mut ctx: Caller<'_, Data>) { 149 | let refs = ctx.data().externrefs.unwrap(); 150 | let assertions = ctx.data_mut().ref_assertions.pop().unwrap(); 151 | assertions(ctx, &refs); 152 | } 153 | 154 | fn assert_refs(mut ctx: Caller<'_, Data>, table: &Table, buffers_liveness: &[bool]) { 155 | let size = table.size(&ctx); 156 | assert_eq!(size, 1 + buffers_liveness.len() as u64); 157 | let refs: Vec<_> = (0..size) 158 | .map(|idx| table.get(&mut ctx, idx).unwrap()) 159 | .collect(); 160 | let refs: Vec<_> = refs.iter().map(Ref::unwrap_extern).collect(); 161 | 162 | let sender_ref = refs[0].as_ref().unwrap(); 163 | assert!(sender_ref.data(&ctx).unwrap().unwrap().is::()); 164 | 165 | for (buffer_ref, &live) in refs[1..].iter().zip(buffers_liveness) { 166 | if live { 167 | let buffer_ref = buffer_ref.as_ref().unwrap(); 168 | assert!(buffer_ref.data(&ctx).unwrap().unwrap().is::>()); 169 | } else { 170 | assert!(buffer_ref.is_none()); 171 | } 172 | } 173 | } 174 | 175 | fn drop_ref(mut ctx: Caller<'_, Data>, dropped: Option>) { 176 | let dropped = dropped.expect("drop fn called with null ref"); 177 | let dropped = dropped.to_manually_rooted(&mut ctx).unwrap(); 178 | ctx.data_mut().dropped.push(dropped); 179 | } 180 | 181 | fn create_linker(engine: &Engine) -> Linker { 182 | let mut linker = Linker::new(engine); 183 | linker 184 | .func_wrap("test", "send_message", send_message) 185 | .unwrap(); 186 | linker 187 | .func_wrap("test", "message_len", message_len) 188 | .unwrap(); 189 | linker 190 | .func_wrap("test", "inspect_refs", inspect_refs) 191 | .unwrap(); 192 | linker.func_wrap("test", "drop_ref", drop_ref).unwrap(); 193 | linker 194 | } 195 | 196 | #[test_casing(8, Product((CompilationProfile::ALL, ["test_export", "test_export_with_casts"])))] 197 | fn transform_module(profile: CompilationProfile, test_export: &str) { 198 | let (_guard, storage) = enable_tracing_assertions(); 199 | 200 | let module = Processor::default() 201 | .set_drop_fn("test", "drop_ref") 202 | .process_bytes(module_bytes(profile)) 203 | .unwrap(); 204 | let module = Module::new(&Engine::default(), module).unwrap(); 205 | let linker = create_linker(module.engine()); 206 | 207 | assert_tracing_output(&storage.lock()); 208 | 209 | let ref_assertions: Vec = vec![ 210 | |caller, table| assert_refs(caller, table, &[]), 211 | |caller, table| assert_refs(caller, table, &[true]), 212 | |caller, table| assert_refs(caller, table, &[true; 2]), 213 | |caller, table| assert_refs(caller, table, &[true; 3]), 214 | |caller, table| assert_refs(caller, table, &[false, true, true]), 215 | |caller, table| assert_refs(caller, table, &[false; 3]), 216 | ]; 217 | let mut store = Store::new(module.engine(), Data::new(ref_assertions)); 218 | let instance = linker.instantiate(&mut store, &module).unwrap(); 219 | let externrefs = instance.get_table(&mut store, "externrefs").unwrap(); 220 | store.data_mut().externrefs = Some(externrefs); 221 | 222 | let exported_fn = instance 223 | .get_typed_func::, ()>(&mut store, test_export) 224 | .unwrap(); 225 | let sender = store.data_mut().push_sender("sender"); 226 | let sender = ExternRef::new(&mut store, sender).unwrap(); 227 | exported_fn.call(&mut store, sender).unwrap(); 228 | 229 | store 230 | .data() 231 | .assert_drops(&store, &["test", "some other string", "42"]); 232 | 233 | store.gc(); 234 | let size = externrefs.size(&store); 235 | assert_eq!(size, 4); // sender + 3 buffers 236 | for i in 0..size { 237 | assert_matches!(externrefs.get(&mut store, i).unwrap(), Ref::Extern(None)); 238 | } 239 | } 240 | 241 | fn assert_tracing_output(storage: &Storage) { 242 | use predicates::{ 243 | ord::{eq, gt}, 244 | str::contains, 245 | }; 246 | use tracing_capture::predicates::{field, into_fn, level, message, name, value, ScanExt}; 247 | 248 | let spans = storage.scan_spans(); 249 | let process_span = spans.single(&name(eq("process"))); 250 | let matches = 251 | level(Level::INFO) & message(eq("parsed custom section")) & field("functions.len", 6_u64); 252 | process_span.scan_events().single(&matches); 253 | 254 | let patch_imports_span = spans.single(&name(eq("patch_imports"))); 255 | let matches = into_fn(message(contains("replaced import")) & level(Level::DEBUG)); 256 | let replaced_imports = patch_imports_span.events().filter_map(|event| { 257 | if matches(&event) { 258 | event.value("name")?.as_str() 259 | } else { 260 | None 261 | } 262 | }); 263 | let replaced_imports: HashSet<_> = replaced_imports.collect(); 264 | assert_eq!( 265 | replaced_imports, 266 | HashSet::from_iter(["externref::insert", "externref::get", "externref::drop"]) 267 | ); 268 | 269 | let replace_functions_span = spans.single(&name(eq("replace_functions"))); 270 | let matches = level(Level::INFO) 271 | & message(contains("replaced calls")) 272 | & field("replaced_count", value(gt(0_u64))); 273 | replace_functions_span.scan_events().single(&matches); 274 | 275 | let transformed_imports = storage.all_spans().filter_map(|span| { 276 | if span.metadata().name() == "transform_import" { 277 | assert_eq!(span["module"].as_str(), Some("test")); 278 | span.value("name")?.as_str() 279 | } else { 280 | None 281 | } 282 | }); 283 | let transformed_imports: HashSet<_> = transformed_imports.collect(); 284 | assert_eq!( 285 | transformed_imports, 286 | HashSet::from_iter(["send_message", "message_len"]) 287 | ); 288 | 289 | let transformed_exports = storage.all_spans().filter_map(|span| { 290 | if span.metadata().name() == "transform_export" { 291 | span.value("name")?.as_str() 292 | } else { 293 | None 294 | } 295 | }); 296 | let transformed_exports: HashSet<_> = transformed_exports.collect(); 297 | assert!( 298 | transformed_exports.contains("test_nulls"), 299 | "{transformed_exports:?}" 300 | ); 301 | 302 | // Since `test_export` and `test_export_with_casts` have the same logic, they may be optimized 303 | // to a single implementation. 304 | let contains_export = transformed_exports.contains("test_export"); 305 | let contains_export_with_casts = transformed_exports.contains("test_export_with_casts"); 306 | assert!( 307 | contains_export || contains_export_with_casts, 308 | "{transformed_exports:?}" 309 | ); 310 | assert_eq!( 311 | transformed_exports.len(), 312 | 2 + contains_export as usize + contains_export_with_casts as usize, 313 | "{transformed_exports:?}" 314 | ); 315 | } 316 | 317 | #[test_casing(4, CompilationProfile::ALL)] 318 | fn null_references(profile: CompilationProfile) { 319 | enable_tracing(); 320 | 321 | let module = Processor::default() 322 | .process_bytes(module_bytes(profile)) 323 | .unwrap(); 324 | let module = Module::new(&Engine::default(), module).unwrap(); 325 | let linker = create_linker(module.engine()); 326 | let mut store = Store::new(module.engine(), Data::new(vec![])); 327 | let instance = linker.instantiate(&mut store, &module).unwrap(); 328 | 329 | let test_fn = instance 330 | .get_typed_func::>, ()>(&mut store, "test_nulls") 331 | .unwrap(); 332 | let sender = store.data_mut().push_sender("sender"); 333 | let sender = ExternRef::new(&mut store, sender).unwrap(); 334 | test_fn.call(&mut store, Some(sender)).unwrap(); 335 | test_fn.call(&mut store, None).unwrap(); 336 | } 337 | --------------------------------------------------------------------------------