├── .githooks └── pre-push ├── .github └── workflows │ ├── ci.yml │ └── wasi-stub-release.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── macro │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── tests │ │ └── test.rs └── wasi-stub │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── lib.rs │ ├── main.rs │ └── parse_args.rs └── examples ├── hello_c ├── .gitignore ├── README.md ├── hello.c └── hello.typ ├── hello_rust ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── hello.typ └── src │ └── lib.rs └── hello_zig ├── .gitignore ├── README.md ├── hello.typ └── hello.zig /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # backup Cargo.lock 6 | CARGO_LOCK_RESTORE_CONTENT=`cat Cargo.lock` 7 | 8 | restore_cargo_lock() 9 | { 10 | echo "$CARGO_LOCK_RESTORE_CONTENT" > Cargo.lock 11 | } 12 | trap restore_cargo_lock EXIT 13 | 14 | cargo +nightly update -Zdirect-minimal-versions -p wasm-minimal-protocol --aggressive 15 | cargo test --workspace --no-run 16 | 17 | restore_cargo_lock 18 | cargo fmt --all --check 19 | cargo clippy --all-targets -- --D warnings 20 | RUSTDOCFLAGS="-D warnings" cargo doc --document-private-items --no-deps 21 | cargo test 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | env: 2 | EM_VERSION: 1.39.18 3 | EM_CACHE_FOLDER: "emsdk-cache" 4 | 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | checks: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup cache 17 | uses: actions/cache@v4 18 | with: 19 | path: ${{env.EM_CACHE_FOLDER}} 20 | key: ${{env.EM_VERSION}}-${{ runner.os }} 21 | - uses: mymindstorm/setup-emsdk@v14 22 | with: 23 | version: ${{env.EM_VERSION}} 24 | actions-cache-folder: ${{env.EM_CACHE_FOLDER}} 25 | - uses: dtolnay/rust-toolchain@stable 26 | with: 27 | targets: wasm32-unknown-unknown, wasm32-wasip1 28 | - uses: mlugg/setup-zig@v1 29 | with: 30 | version: 0.13.0 31 | - uses: typst-community/setup-typst@v4 32 | with: 33 | typst-version: 0.13.1 34 | - name: Cache Rust 35 | uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/.cargo 39 | target/ 40 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 41 | - run: ./.githooks/pre-push 42 | -------------------------------------------------------------------------------- /.github/workflows/wasi-stub-release.yml: -------------------------------------------------------------------------------- 1 | name: Release `wasi-stub` 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: 6 | - master 7 | inputs: 8 | version: 9 | description: "Version number" 10 | required: true 11 | body: 12 | description: "Description of the release" 13 | required: true 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | job: 24 | - { os: ubuntu-latest, target: x86_64-unknown-linux-musl, suffix: "" } 25 | - { os: windows-latest, target: x86_64-pc-windows-msvc, suffix: .exe } 26 | - { os: macos-latest, target: x86_64-apple-darwin, suffix: "" } 27 | name: build ${{ matrix.job.os }} 28 | runs-on: ${{ matrix.job.os }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: dtolnay/rust-toolchain@stable 32 | - run: cargo build --release --package wasi-stub 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: wasi-stub-${{ matrix.job.target }} 36 | path: target/release/wasi-stub${{ matrix.job.suffix }} 37 | 38 | release: 39 | needs: [build] 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Download linux 44 | uses: actions/download-artifact@v4 45 | with: 46 | name: wasi-stub-x86_64-unknown-linux-musl 47 | path: wasi-stub-x86_64-unknown-linux-musl 48 | - name: Download windows 49 | uses: actions/download-artifact@v4 50 | with: 51 | name: wasi-stub-x86_64-pc-windows-msvc 52 | path: wasi-stub-x86_64-pc-windows-msvc 53 | - name: Download macos 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: wasi-stub-x86_64-apple-darwin 57 | path: wasi-stub-x86_64-apple-darwin 58 | - name: Make archives 59 | run: | 60 | tar -cvf wasi-stub-x86_64-unknown-linux-musl.tar.gz wasi-stub-x86_64-unknown-linux-musl/wasi-stub 61 | zip wasi-stub-x86_64-pc-windows-msvc.zip wasi-stub-x86_64-pc-windows-msvc/wasi-stub.exe 62 | tar -cvf wasi-stub-x86_64-apple-darwin.tar.gz wasi-stub-x86_64-apple-darwin/wasi-stub 63 | - name: Create release 64 | uses: softprops/action-gh-release@v2 65 | with: 66 | name: wasi-stub ${{ github.event.inputs.version }} 67 | tag_name: wasi-stub-${{ github.event.inputs.version }} 68 | body: ${{ github.event.inputs.body }} 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | files: | 71 | wasi-stub-x86_64-unknown-linux-musl.tar.gz 72 | wasi-stub-x86_64-pc-windows-msvc.zip 73 | wasi-stub-x86_64-apple-darwin.tar.gz 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/target 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | To run the tests, you will need: 4 | 5 | - The emcc compiler: 6 | - An up-to-date Rust toolchain: 7 | - A zig compiler, version `0.11`: 8 | 9 | Then, you can run the tests with `cargo test`. 10 | 11 | # Git hooks 12 | 13 | To run tests automatically, and check that you don't accidentally bump dependencies of `wasm-minimal-protocol`, add the pre-push [git hook](https://git-scm.com/docs/githooks): 14 | ```sh 15 | git config --local core.hooksPath .githooks 16 | ``` 17 | 18 | The script `.githooks/pre-push` will be run each time you `git push` (except if you use `--no-verify`). -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.75" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.6.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 16 | 17 | [[package]] 18 | name = "bumpalo" 19 | version = "3.16.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 22 | 23 | [[package]] 24 | name = "equivalent" 25 | version = "1.0.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 28 | 29 | [[package]] 30 | name = "hashbrown" 31 | version = "0.14.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 34 | 35 | [[package]] 36 | name = "indexmap" 37 | version = "2.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 40 | dependencies = [ 41 | "equivalent", 42 | "hashbrown", 43 | ] 44 | 45 | [[package]] 46 | name = "leb128" 47 | version = "0.2.5" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 50 | 51 | [[package]] 52 | name = "memchr" 53 | version = "2.6.3" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 56 | 57 | [[package]] 58 | name = "proc-macro2" 59 | version = "1.0.67" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 62 | dependencies = [ 63 | "unicode-ident", 64 | ] 65 | 66 | [[package]] 67 | name = "quote" 68 | version = "1.0.33" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 71 | dependencies = [ 72 | "proc-macro2", 73 | ] 74 | 75 | [[package]] 76 | name = "termcolor" 77 | version = "1.4.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 80 | dependencies = [ 81 | "winapi-util", 82 | ] 83 | 84 | [[package]] 85 | name = "unicode-ident" 86 | version = "1.0.12" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 89 | 90 | [[package]] 91 | name = "unicode-width" 92 | version = "0.1.11" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 95 | 96 | [[package]] 97 | name = "venial" 98 | version = "0.5.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" 101 | dependencies = [ 102 | "proc-macro2", 103 | "quote", 104 | ] 105 | 106 | [[package]] 107 | name = "wasi-stub" 108 | version = "0.2.0" 109 | dependencies = [ 110 | "wasmprinter", 111 | "wast", 112 | ] 113 | 114 | [[package]] 115 | name = "wasm-encoder" 116 | version = "0.219.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" 119 | dependencies = [ 120 | "leb128", 121 | "wasmparser", 122 | ] 123 | 124 | [[package]] 125 | name = "wasm-minimal-protocol" 126 | version = "0.1.0" 127 | dependencies = [ 128 | "proc-macro2", 129 | "quote", 130 | "venial", 131 | ] 132 | 133 | [[package]] 134 | name = "wasmparser" 135 | version = "0.219.1" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" 138 | dependencies = [ 139 | "bitflags", 140 | "indexmap", 141 | ] 142 | 143 | [[package]] 144 | name = "wasmprinter" 145 | version = "0.219.1" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "228cdc1f30c27816da225d239ce4231f28941147d34713dee8f1fff7cb330e54" 148 | dependencies = [ 149 | "anyhow", 150 | "termcolor", 151 | "wasmparser", 152 | ] 153 | 154 | [[package]] 155 | name = "wast" 156 | version = "219.0.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "4f79a9d9df79986a68689a6b40bcc8d5d40d807487b235bebc2ac69a242b54a1" 159 | dependencies = [ 160 | "bumpalo", 161 | "leb128", 162 | "memchr", 163 | "unicode-width", 164 | "wasm-encoder", 165 | ] 166 | 167 | [[package]] 168 | name = "winapi-util" 169 | version = "0.1.9" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 172 | dependencies = [ 173 | "windows-sys", 174 | ] 175 | 176 | [[package]] 177 | name = "windows-sys" 178 | version = "0.59.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 181 | dependencies = [ 182 | "windows-targets", 183 | ] 184 | 185 | [[package]] 186 | name = "windows-targets" 187 | version = "0.52.6" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 190 | dependencies = [ 191 | "windows_aarch64_gnullvm", 192 | "windows_aarch64_msvc", 193 | "windows_i686_gnu", 194 | "windows_i686_gnullvm", 195 | "windows_i686_msvc", 196 | "windows_x86_64_gnu", 197 | "windows_x86_64_gnullvm", 198 | "windows_x86_64_msvc", 199 | ] 200 | 201 | [[package]] 202 | name = "windows_aarch64_gnullvm" 203 | version = "0.52.6" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 206 | 207 | [[package]] 208 | name = "windows_aarch64_msvc" 209 | version = "0.52.6" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 212 | 213 | [[package]] 214 | name = "windows_i686_gnu" 215 | version = "0.52.6" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 218 | 219 | [[package]] 220 | name = "windows_i686_gnullvm" 221 | version = "0.52.6" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 224 | 225 | [[package]] 226 | name = "windows_i686_msvc" 227 | version = "0.52.6" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 230 | 231 | [[package]] 232 | name = "windows_x86_64_gnu" 233 | version = "0.52.6" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 236 | 237 | [[package]] 238 | name = "windows_x86_64_gnullvm" 239 | version = "0.52.6" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 242 | 243 | [[package]] 244 | name = "windows_x86_64_msvc" 245 | version = "0.52.6" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 248 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/macro", "crates/wasi-stub"] 4 | default-members = ["crates/macro", "crates/wasi-stub"] 5 | 6 | [profile.release] 7 | lto = true 8 | panic = "abort" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-minimal-protocol 2 | 3 | A minimal protocol to write [typst plugins](https://typst.app/docs/reference/foundations/plugin/). 4 | 5 | Note that plugins require typst version `0.8` or more. 6 | 7 | ## You want to write a plugin 8 | 9 | A plugin can be written in Rust, C, Zig, or any language than compiles to WebAssembly. 10 | 11 | Rust plugins can use this crate to automatically implement the protocol with a macro: 12 | 13 | ```rust 14 | // Rust file 15 | use wasm_minimal_protocol::*; 16 | 17 | initiate_protocol!(); 18 | 19 | #[wasm_func] 20 | pub fn hello() -> Vec { 21 | b"Hello from wasm!!!".to_vec() 22 | } 23 | ``` 24 | 25 | ```typst 26 | // Typst file 27 | #let p = plugin("/path/to/plugin.wasm") 28 | #assert.eq(str(p.hello()), "Hello from wasm!!!") 29 | ``` 30 | 31 | For other languages, the protocol is described at . You should also take a look at this repository's [examples](#examples). 32 | 33 | ## Examples 34 | 35 | See the example for your language: 36 | 37 | - [Rust](examples/hello_rust/) 38 | - [Zig](examples/hello_zig/) 39 | - [C](examples/hello_c/) 40 | 41 | If you have all the required dependencies, you may build all examples by running `cargo test`. 42 | 43 | If you want to pass structured data to Typst, check how it's done with the [rust example using cbor](examples/hello_rust/). 44 | 45 | ## wasi-stub 46 | 47 | The runtime used by typst do not allow the plugin to import any function (beside the ones used by the protocol). In particular, if your plugin is compiled for [WASI](https://wasi.dev/), it will not be able to be loaded by typst. 48 | 49 | To get around that, you can use [wasi-stub](./crates/wasi-stub). It will detect all WASI-related imports, and replace them by stubs that do nothing. 50 | 51 | If you are compiling C code with `emcc`, stubbing is almost certainly required. 52 | -------------------------------------------------------------------------------- /crates/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-minimal-protocol" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license-file = "LICENSE" 6 | description = "Typst plugin helper macro library" 7 | repository = "https://github.com/astrale-sharp/wasm-minimal-protocol" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0.36" 14 | quote = "1.0.15" 15 | venial = "0.5.0" 16 | -------------------------------------------------------------------------------- /crates/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Minimal protocol for sending/receiving messages from and to a wasm host. 2 | //! 3 | //! If you define a function accepting `n` arguments of type `&[u8]`, it will 4 | //! internally be exported as a function accepting `n` integers. 5 | //! 6 | //! # Example 7 | //! 8 | //! ``` 9 | //! use wasm_minimal_protocol::wasm_func; 10 | //! 11 | //! #[cfg(target_arch = "wasm32")] 12 | //! wasm_minimal_protocol::initiate_protocol!(); 13 | //! 14 | //! #[cfg_attr(target_arch = "wasm32", wasm_func)] 15 | //! fn concatenate(arg1: &[u8], arg2: &[u8]) -> Vec { 16 | //! [arg1, arg2].concat() 17 | //! } 18 | //! ``` 19 | //! 20 | //! # Allowed types 21 | //! 22 | //! Allowed input types are either `&[u8]` or `&mut [u8]`. 23 | //! 24 | //! Allowed output types are 25 | //! - `Vec` 26 | //! - `Box<[u8]>` 27 | //! - `&[u8]` 28 | //! - `Result`, where `T` is any of the above, and `E` is a type implementing 29 | //! [`Display`](std::fmt::Display). 30 | //! 31 | //! # Protocol 32 | //! 33 | //! The specification of the low-level protocol can be found in the typst documentation: 34 | //! 35 | 36 | use proc_macro::TokenStream; 37 | use proc_macro2::{Delimiter, TokenTree}; 38 | use quote::{format_ident, quote, ToTokens}; 39 | use venial::*; 40 | 41 | /// Macro that sets up the correct imports and traits to be used by [`macro@wasm_func`]. 42 | /// 43 | /// This macro should be called only once, preferably at the root of the crate. It does 44 | /// not take any arguments. 45 | #[proc_macro] 46 | pub fn initiate_protocol(stream: TokenStream) -> TokenStream { 47 | if !stream.is_empty() { 48 | return quote!( 49 | compile_error!("This macro does not take any arguments"); 50 | ) 51 | .into(); 52 | } 53 | quote!( 54 | #[link(wasm_import_module = "typst_env")] 55 | extern "C" { 56 | #[link_name = "wasm_minimal_protocol_send_result_to_host"] 57 | fn __send_result_to_host(ptr: *const u8, len: usize); 58 | #[link_name = "wasm_minimal_protocol_write_args_to_buffer"] 59 | fn __write_args_to_buffer(ptr: *mut u8); 60 | } 61 | 62 | trait __ToResult { 63 | type Ok: ::core::convert::AsRef<[u8]>; 64 | type Err: ::core::fmt::Display; 65 | fn to_result(self) -> ::core::result::Result; 66 | } 67 | impl __ToResult for Vec { 68 | type Ok = Self; 69 | type Err = ::core::convert::Infallible; 70 | fn to_result(self) -> ::core::result::Result { 71 | Ok(self) 72 | } 73 | } 74 | impl __ToResult for Box<[u8]> { 75 | type Ok = Self; 76 | type Err = ::core::convert::Infallible; 77 | fn to_result(self) -> ::core::result::Result { 78 | Ok(self) 79 | } 80 | } 81 | impl<'a> __ToResult for &'a [u8] { 82 | type Ok = Self; 83 | type Err = ::core::convert::Infallible; 84 | fn to_result(self) -> ::core::result::Result { 85 | Ok(self) 86 | } 87 | } 88 | impl, E: ::core::fmt::Display> __ToResult for ::core::result::Result { 89 | type Ok = T; 90 | type Err = E; 91 | fn to_result(self) -> Self { 92 | self 93 | } 94 | } 95 | ) 96 | .into() 97 | } 98 | 99 | /// Wrap the function to be used with the [protocol](https://typst.app/docs/reference/foundations/plugin/#protocol). 100 | /// 101 | /// # Arguments 102 | /// 103 | /// All the arguments of the function should be `&[u8]`, no lifetime needed. 104 | /// 105 | /// # Return type 106 | /// 107 | /// The return type of the function should be `Vec` or `Result, E>` where 108 | /// `E: ToString`. 109 | /// 110 | /// If the function return `Vec`, it will be implicitely wrapped in `Ok`. 111 | /// 112 | /// # Example 113 | /// 114 | /// ``` 115 | /// use wasm_minimal_protocol::wasm_func; 116 | /// 117 | /// #[cfg(target_arch = "wasm32")] 118 | /// wasm_minimal_protocol::initiate_protocol!(); 119 | /// 120 | /// #[cfg_attr(target_arch = "wasm32", wasm_func)] 121 | /// fn function_one() -> Vec { 122 | /// Vec::new() 123 | /// } 124 | /// 125 | /// #[cfg_attr(target_arch = "wasm32", wasm_func)] 126 | /// fn function_two(arg1: &[u8], arg2: &[u8]) -> Result, i32> { 127 | /// Ok(b"Normal message".to_vec()) 128 | /// } 129 | /// 130 | /// #[cfg_attr(target_arch = "wasm32", wasm_func)] 131 | /// fn function_three(arg1: &[u8]) -> Result, String> { 132 | /// Err(String::from("Error message")) 133 | /// } 134 | /// ``` 135 | #[proc_macro_attribute] 136 | pub fn wasm_func(_: TokenStream, item: TokenStream) -> TokenStream { 137 | let mut item = proc_macro2::TokenStream::from(item); 138 | let decl = parse_declaration(item.clone()).expect("invalid declaration"); 139 | let func = match decl.as_function() { 140 | Some(func) => func.clone(), 141 | None => { 142 | let error = venial::Error::new_at_tokens( 143 | &item, 144 | "#[wasm_func] can only be applied to a function", 145 | ); 146 | item.extend(error.to_compile_error()); 147 | return item.into(); 148 | } 149 | }; 150 | let Function { 151 | name, 152 | params, 153 | vis_marker, 154 | .. 155 | } = func.clone(); 156 | 157 | let mut error = None; 158 | 159 | let p = params 160 | .items() 161 | .filter_map(|x| match x { 162 | FnParam::Receiver(_p) => { 163 | let x = x.to_token_stream(); 164 | error = Some(venial::Error::new_at_tokens( 165 | &x, 166 | format!("the `{x}` argument is not allowed by the protocol"), 167 | )); 168 | None 169 | } 170 | FnParam::Typed(p) => { 171 | if !tokens_are_slice(&p.ty.tokens) { 172 | let p_to_string = p.ty.to_token_stream(); 173 | error = Some(venial::Error::new_at_tokens( 174 | &p_to_string, 175 | format!("only parameters of type `&[u8]` or `&mut [u8]` are allowed, not {p_to_string}"), 176 | )); 177 | None 178 | } else { 179 | Some(p.name.clone()) 180 | } 181 | } 182 | }) 183 | .collect::>(); 184 | let p_len = p 185 | .iter() 186 | .map(|name| format_ident!("__{}_len", name)) 187 | .collect::>(); 188 | 189 | let mut get_unsplit_params = quote!( 190 | let __total_len = #(#p_len + )* 0; 191 | let mut __unsplit_params = vec![0u8; __total_len]; 192 | unsafe { __write_args_to_buffer(__unsplit_params.as_mut_ptr()); } 193 | let __unsplit_params: &mut [u8] = &mut __unsplit_params; 194 | ); 195 | let mut set_args = quote!( 196 | let start: usize = 0; 197 | ); 198 | match p.len() { 199 | 0 => get_unsplit_params = quote!(), 200 | 1 => { 201 | let arg = p.first().unwrap(); 202 | set_args = quote!( 203 | let #arg: &mut [u8] = __unsplit_params; 204 | ) 205 | } 206 | _ => { 207 | let mut sets = vec![]; 208 | for (idx, (arg_name, arg_len)) in p 209 | .iter() 210 | .zip(p.iter().map(|name| format_ident!("__{}_len", &name))) 211 | .enumerate() 212 | { 213 | if idx == p.len() - 1 { 214 | sets.push(quote!( 215 | let #arg_name: &mut [u8] = __unsplit_params; 216 | )); 217 | } else { 218 | sets.push(quote!( 219 | let (#arg_name, __unsplit_params): (&mut [u8], &mut [u8]) = 220 | __unsplit_params.split_at_mut(#arg_len); 221 | )); 222 | } 223 | } 224 | set_args = quote!( 225 | #( 226 | #sets 227 | )* 228 | ); 229 | } 230 | } 231 | 232 | let inner_name = format_ident!("__wasm_minimal_protocol_internal_function_{}", name); 233 | let export_name = proc_macro2::Literal::string(&name.to_string()); 234 | 235 | let mut result = quote!(#func); 236 | if let Some(error) = error { 237 | result.extend(error.to_compile_error()); 238 | } else { 239 | result.extend(quote!( 240 | #[export_name = #export_name] 241 | #vis_marker extern "C" fn #inner_name(#(#p_len: usize),*) -> i32 { 242 | #get_unsplit_params 243 | #set_args 244 | 245 | let result = #name(#(#p),*); 246 | let result = __ToResult::to_result(result); 247 | let err_vec: Vec; 248 | let (message, code) = match result { 249 | Ok(ref s) => (s.as_ref(), 0), 250 | Err(err) => { 251 | err_vec = err.to_string().into_bytes(); 252 | (err_vec.as_slice(), 1) 253 | }, 254 | }; 255 | unsafe { __send_result_to_host(message.as_ptr(), message.len()); } 256 | code 257 | } 258 | )) 259 | } 260 | result.into() 261 | } 262 | 263 | /// Check that `ty` is either `&[u8]` or `&mut [u8]`. 264 | fn tokens_are_slice(ty: &[TokenTree]) -> bool { 265 | let is_ampersand = |t: &_| matches!(t, TokenTree::Punct(punct) if punct.as_char() == '&'); 266 | let is_quote = |t: &_| matches!(t, TokenTree::Punct(punct) if punct.as_char() == '\''); 267 | let is_sym = |t: &_| matches!(t, TokenTree::Ident(_)); 268 | let is_mut = |t: &_| matches!(t, TokenTree::Ident(i) if i == "mut"); 269 | let is_sliceu8 = |t: &_| match t { 270 | TokenTree::Group(group) => { 271 | group.delimiter() == Delimiter::Bracket && { 272 | let mut inner = group.stream().into_iter(); 273 | matches!( 274 | inner.next(), 275 | Some(proc_macro2::TokenTree::Ident(i)) if i == "u8" 276 | ) && inner.next().is_none() 277 | } 278 | } 279 | _ => false, 280 | }; 281 | let mut iter = ty.iter(); 282 | let Some(amp) = iter.next() else { return false }; 283 | if !is_ampersand(amp) { 284 | return false; 285 | } 286 | let Some(mut next) = iter.next() else { 287 | return false; 288 | }; 289 | if is_quote(next) { 290 | // there is a lifetime 291 | let Some(lft) = iter.next() else { return false }; 292 | if !is_sym(lft) { 293 | return false; 294 | } 295 | match iter.next() { 296 | Some(t) => next = t, 297 | None => return false, 298 | } 299 | } 300 | if is_mut(next) { 301 | // the slice is mutable 302 | match iter.next() { 303 | Some(t) => next = t, 304 | None => return false, 305 | } 306 | } 307 | if !is_sliceu8(next) { 308 | return false; 309 | } 310 | if iter.next().is_some() { 311 | return false; 312 | } 313 | 314 | true 315 | } 316 | -------------------------------------------------------------------------------- /crates/macro/tests/test.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | process::Command, 4 | }; 5 | 6 | fn wasi_stub(path: PathBuf) { 7 | let path = path.canonicalize().unwrap(); 8 | 9 | let wasi_stub = Command::new("cargo") 10 | .arg("run") 11 | .arg(&path) 12 | .arg("-o") 13 | .arg(&path) 14 | .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../wasi-stub")) 15 | .status() 16 | .unwrap(); 17 | if !wasi_stub.success() { 18 | panic!("wasi-stub failed"); 19 | } 20 | } 21 | 22 | fn typst_compile(path: &Path) { 23 | let typst_version = Command::new("typst").arg("--version").output().unwrap(); 24 | if !typst_version.status.success() { 25 | panic!("typst --version failed"); 26 | } 27 | let version_string = match String::from_utf8(typst_version.stdout) { 28 | Ok(s) => s, 29 | Err(err) => panic!("failed to parse typst version: {err}"), 30 | }; 31 | if let Some(s) = version_string.strip_prefix("typst ") { 32 | let version = s.split('.').collect::>(); 33 | let [major, minor, _] = version.as_slice() else { 34 | panic!("failed to parse version string {version_string}") 35 | }; 36 | if !(major.parse::().unwrap() >= 1 || minor.parse::().unwrap() >= 8) { 37 | panic!("The typst version is too low for plugin: you need at least 0.8.0"); 38 | } 39 | } 40 | 41 | let path = PathBuf::from(path).canonicalize().unwrap(); 42 | let typst_compile = Command::new("typst") 43 | .arg("compile") 44 | .arg("hello.typ") 45 | .current_dir(path) 46 | .status() 47 | .unwrap(); 48 | if !typst_compile.success() { 49 | panic!("typst compile failed"); 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_c() { 55 | let dir_path = Path::new(concat!( 56 | env!("CARGO_MANIFEST_DIR"), 57 | "/../../examples/hello_c" 58 | )); 59 | 60 | let build_c = Command::new("emcc") 61 | .arg("--no-entry") 62 | .arg("-O3") 63 | .arg("-s") 64 | .arg("ERROR_ON_UNDEFINED_SYMBOLS=0") 65 | .arg("-o") 66 | .arg("hello.wasm") 67 | .arg("hello.c") 68 | .current_dir(dir_path) 69 | .status() 70 | .unwrap(); 71 | if !build_c.success() { 72 | panic!("Compiling with emcc failed"); 73 | } 74 | wasi_stub(dir_path.join("hello.wasm")); 75 | typst_compile(dir_path); 76 | } 77 | 78 | #[test] 79 | fn test_rust() { 80 | let dir_path = Path::new(concat!( 81 | env!("CARGO_MANIFEST_DIR"), 82 | "/../../examples/hello_rust" 83 | )); 84 | 85 | for target in ["wasm32-unknown-unknown", "wasm32-wasip1"] { 86 | let build_rust = Command::new("cargo") 87 | .arg("build") 88 | .arg("--release") 89 | .arg("--target") 90 | .arg(target) 91 | .current_dir(dir_path) 92 | .status() 93 | .unwrap(); 94 | if !build_rust.success() { 95 | panic!("Compiling with cargo failed"); 96 | } 97 | std::fs::copy( 98 | dir_path 99 | .join("target") 100 | .join(target) 101 | .join("release/hello.wasm"), 102 | dir_path.join("hello.wasm"), 103 | ) 104 | .unwrap(); 105 | if target == "wasm32-wasip1" { 106 | wasi_stub(dir_path.join("hello.wasm")); 107 | } 108 | typst_compile(dir_path); 109 | } 110 | } 111 | 112 | #[test] 113 | fn test_zig() { 114 | let dir_path = Path::new(concat!( 115 | env!("CARGO_MANIFEST_DIR"), 116 | "/../../examples/hello_zig" 117 | )); 118 | 119 | for target in ["wasm32-freestanding", "wasm32-wasi"] { 120 | let build_zig = Command::new("zig") 121 | .arg("build-exe") 122 | .arg("hello.zig") 123 | .arg("-target") 124 | .arg(target) 125 | .arg("-fno-entry") 126 | .arg("-O") 127 | .arg("ReleaseSmall") 128 | .arg("--export=hello") 129 | .arg("--export=double_it") 130 | .arg("--export=concatenate") 131 | .arg("--export=shuffle") 132 | .arg("--export=returns_ok") 133 | .arg("--export=returns_err") 134 | .arg("--export=will_panic") 135 | .current_dir(dir_path) 136 | .status() 137 | .unwrap(); 138 | if !build_zig.success() { 139 | panic!("Compiling with zig failed"); 140 | } 141 | if target == "wasm32-wasi" { 142 | wasi_stub(dir_path.join("hello.wasm")); 143 | } 144 | typst_compile(dir_path); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /crates/wasi-stub/.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /crates/wasi-stub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasi-stub" 3 | edition = "2021" 4 | version = "0.2.0" 5 | authors = ["Arnaud Golfouse "] 6 | repository = "https://github.com/astrale-sharp/wasm-minimal-protocol" 7 | 8 | [dependencies] 9 | wast = "219.0" 10 | wasmprinter = "0.219" 11 | -------------------------------------------------------------------------------- /crates/wasi-stub/README.md: -------------------------------------------------------------------------------- 1 | # Wasi Stub 2 | 3 | This is a tool allowing you to take a [wasi](https://wasi.dev/) compliant WebAssembly file and replace all functions wasi depends on by meaningless stubs. 4 | 5 | If you don't depend on printing or reading/writing files, your code will probably still work and it will now be compatible with typst or host-wasmi. 6 | 7 | ## How to install 8 | 9 | From the wasi-stub directory (where this README is), run `cargo install --path .`, you will need a working rust toolchain. 10 | 11 | ## How to use 12 | 13 | Once you installed wasi-stub, you can simply run `wasi-stub my_library.wasm` from the terminal. 14 | 15 | # Alternatives (?) 16 | 17 | Inspiration for this comes from [https://github.com/dicej/stubber]. It replaces stubbed functions with a trap, while `wasi-stub` replaces them with functions that do nothing. 18 | -------------------------------------------------------------------------------- /crates/wasi-stub/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use wast::{ 4 | core::{ 5 | Expression, Func, FuncKind, FunctionType, HeapType, InlineExport, InnerTypeKind, 6 | Instruction, ItemKind, Local, ModuleField, ModuleKind, RefType, TypeUse, ValType, 7 | }, 8 | token::{Id, Index, NameAnnotation}, 9 | Wat, 10 | }; 11 | 12 | pub enum FunctionsToStub { 13 | All, 14 | Some(HashSet), 15 | } 16 | pub struct ShouldStub { 17 | pub modules: HashMap, 18 | } 19 | impl Default for ShouldStub { 20 | fn default() -> Self { 21 | Self { 22 | modules: [(String::from("wasi_snapshot_preview1"), FunctionsToStub::All)] 23 | .into_iter() 24 | .collect(), 25 | } 26 | } 27 | } 28 | 29 | enum ImportIndex { 30 | ToStub(u32), 31 | Keep(u32), 32 | } 33 | 34 | struct ToStub { 35 | fields_index: usize, 36 | span: wast::token::Span, 37 | nb_results: usize, 38 | ty: TypeUse<'static, FunctionType<'static>>, 39 | name: Option>, 40 | id: Option>, 41 | locals: Vec>, 42 | } 43 | 44 | impl ShouldStub { 45 | fn should_stub(&self, module: &str, function: &str) -> bool { 46 | if let Some(functions) = self.modules.get(module) { 47 | match functions { 48 | FunctionsToStub::All => true, 49 | FunctionsToStub::Some(functions) => functions.contains(function), 50 | } 51 | } else { 52 | false 53 | } 54 | } 55 | } 56 | 57 | fn static_id(id: Option) -> Option> { 58 | id.map(|id| { 59 | let mut name = id.name().to_owned(); 60 | name.insert(0, '$'); 61 | let parser = Box::leak(Box::new( 62 | wast::parser::ParseBuffer::new(name.leak()).unwrap(), 63 | )); 64 | wast::parser::parse::(parser).unwrap() 65 | }) 66 | } 67 | fn static_name_annotation(name: Option) -> Option> { 68 | name.map(|name| NameAnnotation { 69 | name: String::from(name.name).leak(), 70 | }) 71 | } 72 | 73 | pub fn stub_wasi_functions( 74 | binary: &[u8], 75 | should_stub: ShouldStub, 76 | return_value: u32, 77 | ) -> crate::Result> { 78 | let wat = wasmprinter::print_bytes(binary).map_err(std::io::Error::other)?; 79 | let parse_buffer = wast::parser::ParseBuffer::new(&wat)?; 80 | 81 | let mut wat: Wat = wast::parser::parse(&parse_buffer)?; 82 | let module = match &mut wat { 83 | Wat::Module(m) => m, 84 | Wat::Component(_) => return Err(Error::message("components are not supported")), 85 | }; 86 | let fields = match &mut module.kind { 87 | ModuleKind::Text(f) => f, 88 | ModuleKind::Binary(_) => { 89 | println!("[WARNING] binary directives are not supported"); 90 | return Ok(binary.to_owned()); 91 | } 92 | }; 93 | 94 | let mut types = Vec::new(); 95 | let mut imports = Vec::new(); 96 | let mut to_stub = Vec::new(); 97 | let mut insert_stubs_index = None; 98 | let mut new_import_indices = Vec::new(); 99 | 100 | for (field_idx, field) in fields.iter_mut().enumerate() { 101 | match field { 102 | ModuleField::Type(t) => types.push(t), 103 | ModuleField::Import(i) => { 104 | let typ = match &i.item.kind { 105 | ItemKind::Func(typ) => typ.index.and_then(|index| match index { 106 | Index::Num(index, _) => Some(index as usize), 107 | Index::Id(_) => None, 108 | }), 109 | _ => None, 110 | }; 111 | let new_index = match typ { 112 | Some(type_index) if should_stub.should_stub(i.module, i.field) => { 113 | println!("Stubbing function {}::{}", i.module, i.field); 114 | let typ = &types[type_index]; 115 | let ty = TypeUse::new_with_index(Index::Num(type_index as u32, typ.span)); 116 | let wast::core::TypeDef { 117 | kind: InnerTypeKind::Func(func_typ), 118 | .. 119 | } = &typ.def 120 | else { 121 | continue; 122 | }; 123 | let id = static_id(i.item.id); 124 | let locals: Vec = func_typ 125 | .params 126 | .iter() 127 | .map(|(id, name, val_type)| Local { 128 | id: static_id(*id), 129 | name: static_name_annotation(*name), 130 | // FIXME: This long match dance is _only_ to make the lifetime of ty 'static. A lot of things have to go through this dance (see the `static_*` function...) 131 | // Instead, we should write the new function here, in place, by replacing `field`. This is currently done in the for loop at the veryend of this function. 132 | // THEN, at the end of the loop, swap every function in it's right place. No need to do more ! 133 | ty: match val_type { 134 | ValType::I32 => ValType::I32, 135 | ValType::I64 => ValType::I64, 136 | ValType::F32 => ValType::F32, 137 | ValType::F64 => ValType::F64, 138 | ValType::V128 => ValType::V128, 139 | ValType::Ref(r) => ValType::Ref(RefType { 140 | nullable: r.nullable, 141 | heap: match r.heap { 142 | HeapType::Concrete(index) => { 143 | HeapType::Concrete(match index { 144 | Index::Num(n, s) => Index::Num(n, s), 145 | Index::Id(id) => { 146 | Index::Id(static_id(Some(id)).unwrap()) 147 | } 148 | }) 149 | } 150 | HeapType::Abstract { shared, ty } => { 151 | HeapType::Abstract { shared, ty } 152 | } 153 | }, 154 | }), 155 | }, 156 | }) 157 | .collect(); 158 | to_stub.push(ToStub { 159 | fields_index: field_idx, 160 | span: i.span, 161 | nb_results: func_typ.results.len(), 162 | ty, 163 | name: i.item.name.map(|n| NameAnnotation { 164 | name: n.name.to_owned().leak(), 165 | }), 166 | id, 167 | locals, 168 | }); 169 | ImportIndex::ToStub(to_stub.len() as u32 - 1) 170 | } 171 | _ => { 172 | imports.push(i); 173 | ImportIndex::Keep(imports.len() as u32 - 1) 174 | } 175 | }; 176 | new_import_indices.push(new_index); 177 | } 178 | ModuleField::Func(func) => { 179 | if insert_stubs_index.is_none() { 180 | insert_stubs_index = Some(field_idx); 181 | } 182 | match &mut func.kind { 183 | FuncKind::Import(f) => { 184 | if should_stub.should_stub(f.module, f.field) { 185 | println!("[WARNING] Stubbing inline function is not yet supported"); 186 | println!( 187 | "[WARNING] ignoring inline function \"{}\" \"{}\"", 188 | f.module, f.field 189 | ); 190 | } 191 | } 192 | FuncKind::Inline { expression, .. } => { 193 | for inst in expression.instrs.as_mut().iter_mut() { 194 | match inst { 195 | Instruction::RefFunc(Index::Num(index, _)) 196 | | Instruction::ReturnCall(Index::Num(index, _)) 197 | | Instruction::Call(Index::Num(index, _)) => { 198 | if let Some(new_index) = new_import_indices.get(*index as usize) 199 | { 200 | *index = match new_index { 201 | ImportIndex::ToStub(idx) => *idx + imports.len() as u32, 202 | ImportIndex::Keep(idx) => *idx, 203 | }; 204 | } 205 | } 206 | _ => {} 207 | } 208 | } 209 | } 210 | } 211 | } 212 | _ => {} 213 | } 214 | } 215 | drop(imports); 216 | drop(types); 217 | 218 | let insert_stubs_index = insert_stubs_index 219 | .expect("This is weird: there are no code sections in this wasm executable !"); 220 | 221 | for ( 222 | already_stubbed, 223 | ToStub { 224 | fields_index, 225 | span, 226 | nb_results, 227 | ty, 228 | name, 229 | id, 230 | locals, 231 | }, 232 | ) in to_stub.into_iter().enumerate() 233 | { 234 | let instructions = { 235 | let mut res = Vec::with_capacity(nb_results); 236 | for _ in 0..nb_results { 237 | // Weird value, hopefully this makes it easier to track usage of these stubbed functions. 238 | res.push(Instruction::I32Const(return_value as i32)); 239 | } 240 | res 241 | }; 242 | let function = Func { 243 | span, 244 | id, 245 | name, 246 | // no exports 247 | exports: InlineExport { names: Vec::new() }, 248 | kind: wast::core::FuncKind::Inline { 249 | locals: locals.into_boxed_slice(), 250 | expression: Expression { 251 | instrs: instructions.into_boxed_slice(), 252 | branch_hints: Box::new([]), 253 | instr_spans: None, 254 | }, 255 | }, 256 | ty, 257 | }; 258 | fields.insert(insert_stubs_index, ModuleField::Func(function)); 259 | fields.remove(fields_index - already_stubbed); 260 | } 261 | 262 | Ok(module.encode()?) 263 | } 264 | 265 | // Error handling 266 | pub struct Error(Box); 267 | impl Error { 268 | pub fn message(reason: impl AsRef) -> Self { 269 | Self(Box::new(std::io::Error::other(reason.as_ref().to_string()))) 270 | } 271 | } 272 | impl std::fmt::Debug for Error { 273 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 274 | std::fmt::Display::fmt(&self.0, f) 275 | } 276 | } 277 | impl From for Error 278 | where 279 | E: std::error::Error + Send + Sync + 'static, 280 | { 281 | fn from(err: E) -> Self { 282 | Self(Box::new(err)) 283 | } 284 | } 285 | pub type Result = std::result::Result; 286 | -------------------------------------------------------------------------------- /crates/wasi-stub/src/main.rs: -------------------------------------------------------------------------------- 1 | mod parse_args; 2 | 3 | use std::path::PathBuf; 4 | use wasi_stub::{stub_wasi_functions, Error, Result}; 5 | 6 | fn main() -> Result<()> { 7 | let parse_args::Args { 8 | binary, 9 | path, 10 | output_path, 11 | list, 12 | should_stub, 13 | return_value, 14 | } = parse_args::Args::new()?; 15 | 16 | let output = stub_wasi_functions(&binary, should_stub, return_value)?; 17 | 18 | if !list { 19 | write_output(path, output_path, output)?; 20 | } else { 21 | println!("NOTE: no output produced because the '--list' option was specified") 22 | } 23 | 24 | Ok(()) 25 | } 26 | 27 | fn write_output(path: PathBuf, output_path: Option, output: Vec) -> Result<()> { 28 | let output_path = match output_path { 29 | Some(p) => p, 30 | // Try to find an unused output path 31 | None => { 32 | let mut i = 0; 33 | let mut file_name = path.file_stem().unwrap().to_owned(); 34 | file_name.push(" - stubbed.wasm"); 35 | loop { 36 | let mut new_path = path.clone(); 37 | if i > 0 { 38 | let mut file_name = path.file_stem().unwrap().to_owned(); 39 | file_name.push(format!(" - stubbed ({i}).wasm")); 40 | new_path.set_file_name(&file_name); 41 | } else { 42 | new_path.set_file_name(&file_name); 43 | } 44 | if !new_path.exists() { 45 | break new_path; 46 | } 47 | i += 1; 48 | } 49 | } 50 | }; 51 | std::fs::write(&output_path, output)?; 52 | let permissions = std::fs::File::open(path)?.metadata()?.permissions(); 53 | std::fs::File::open(output_path)?.set_permissions(permissions)?; 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /crates/wasi-stub/src/parse_args.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use std::{ 3 | collections::{HashMap, HashSet}, 4 | ffi::OsString, 5 | path::PathBuf, 6 | }; 7 | use wasi_stub::{FunctionsToStub, ShouldStub}; 8 | 9 | pub(crate) struct Args { 10 | pub binary: Vec, 11 | pub path: PathBuf, 12 | pub output_path: Option, 13 | pub list: bool, 14 | pub should_stub: ShouldStub, 15 | pub return_value: u32, 16 | } 17 | 18 | enum Arg { 19 | Plain { 20 | name: &'static str, 21 | required: bool, 22 | help: &'static str, 23 | }, 24 | LongFlag { 25 | name: &'static str, 26 | help: &'static str, 27 | }, 28 | #[allow(dead_code)] 29 | ShortFlag { flag: char, help: &'static str }, 30 | KeyValue { 31 | keys: &'static [&'static str], 32 | value_type: &'static str, 33 | help: &'static str, 34 | }, 35 | } 36 | 37 | struct TestArgParser { 38 | command_name: &'static str, 39 | help: &'static str, 40 | args: Vec, 41 | plain_args: HashMap, 42 | long_flags: HashSet, 43 | short_flags: HashSet, 44 | key_values: HashMap, 45 | requested_help: bool, 46 | } 47 | impl TestArgParser { 48 | fn new(command_name: &'static str, help: &'static str, args: Vec) -> Self { 49 | Self { 50 | command_name, 51 | help, 52 | args, 53 | plain_args: Default::default(), 54 | long_flags: Default::default(), 55 | short_flags: Default::default(), 56 | key_values: Default::default(), 57 | requested_help: false, 58 | } 59 | } 60 | 61 | fn parse(&mut self) -> Result<(), String> { 62 | let mut expect_long_flags = HashSet::new(); 63 | let mut expect_short_flags = HashSet::new(); 64 | let mut expect_keys = HashSet::new(); 65 | let mut remaining_plain_args: Vec<_> = self 66 | .args 67 | .iter() 68 | .filter_map(|a| { 69 | match a { 70 | Arg::Plain { name, required, .. } => return Some((*name, *required)), 71 | Arg::LongFlag { name, .. } => { 72 | expect_long_flags.insert(*name); 73 | } 74 | Arg::ShortFlag { flag, .. } => { 75 | expect_short_flags.insert(*flag); 76 | } 77 | Arg::KeyValue { keys, .. } => { 78 | for key in *keys { 79 | expect_keys.insert(*key); 80 | } 81 | } 82 | } 83 | None 84 | }) 85 | .collect(); 86 | 87 | let mut current_key = None; 88 | for arg in std::env::args_os().skip(1) { 89 | if let Some(key) = current_key.take() { 90 | self.key_values.insert(key, arg); 91 | continue; 92 | } 93 | let arg = match arg.to_str() { 94 | Some(a) => a, 95 | None => { 96 | if let Some(plain) = remaining_plain_args.pop() { 97 | self.plain_args.insert(plain.0.to_owned(), arg); 98 | continue; 99 | } else { 100 | return Err(format!("Unexpected argument: '{arg:?}'")); 101 | } 102 | } 103 | }; 104 | if arg == "--help" || arg == "-h" { 105 | self.requested_help = true; 106 | continue; 107 | } 108 | if expect_long_flags.contains(arg) { 109 | self.long_flags.insert(arg.to_owned()); 110 | } else if expect_keys.contains(arg) { 111 | current_key = Some(arg.to_owned()); 112 | } else if arg.starts_with('-') { 113 | for c in arg.chars().skip(1) { 114 | if expect_short_flags.contains(&c) { 115 | self.short_flags.insert(c); 116 | } else { 117 | return Err(format!("Unknown short option: {c}")); 118 | } 119 | } 120 | } else if let Some(plain) = remaining_plain_args.pop() { 121 | self.plain_args.insert(plain.0.to_owned(), arg.into()); 122 | } else { 123 | return Err(format!("Unexpected argument: '{arg}'")); 124 | } 125 | } 126 | if !self.requested_help { 127 | for (plain_arg, required) in remaining_plain_args { 128 | if required { 129 | return Err(format!("Missing argument {plain_arg}")); 130 | } 131 | } 132 | } 133 | Ok(()) 134 | } 135 | 136 | fn print_help_message(&self) { 137 | println!("{} {}", self.command_name, env!("CARGO_PKG_VERSION")); 138 | println!(); 139 | println!("{}", self.help); 140 | println!(); 141 | println!("USAGE:"); 142 | print!(" {} [OPTIONS]", self.command_name); 143 | for (name, required) in self.args.iter().filter_map(|a| match a { 144 | Arg::Plain { name, required, .. } => Some((*name, *required)), 145 | _ => None, 146 | }) { 147 | if required { 148 | print!(" <{}>", name) 149 | } else { 150 | print!(" [{}]", name) 151 | } 152 | } 153 | println!(); 154 | println!(); 155 | for arg in &self.args { 156 | match arg { 157 | Arg::Plain { 158 | name, 159 | required, 160 | help, 161 | } => { 162 | if *required { 163 | println!(" <{name}>"); 164 | } else { 165 | println!(" [{name}]"); 166 | } 167 | Self::print_help(help); 168 | } 169 | _ => continue, 170 | } 171 | } 172 | println!(); 173 | println!("OPTIONS:"); 174 | for arg in &self.args { 175 | match arg { 176 | Arg::Plain { .. } => continue, 177 | Arg::LongFlag { name, help } => { 178 | println!(" {name}"); 179 | Self::print_help(help); 180 | } 181 | Arg::ShortFlag { flag, help } => { 182 | println!(" -{flag}"); 183 | Self::print_help(help); 184 | } 185 | Arg::KeyValue { 186 | keys, 187 | value_type, 188 | help, 189 | } => { 190 | print!(" "); 191 | for (i, key) in keys.iter().enumerate() { 192 | if i != 0 { 193 | print!(", "); 194 | } 195 | print!("{key}") 196 | } 197 | println!(" <{value_type}>"); 198 | Self::print_help(help); 199 | } 200 | } 201 | } 202 | } 203 | 204 | fn print_help(help: &str) { 205 | for line in help.lines() { 206 | println!(" {line}"); 207 | } 208 | if !help.contains('\n') { 209 | println!(); 210 | } 211 | } 212 | } 213 | 214 | impl Args { 215 | pub fn new() -> Result { 216 | let mut arg_parser = TestArgParser::new( 217 | env!("CARGO_PKG_NAME"), 218 | "A command to replace wasi functions with stubs. The stubbed function can still be called, but they won't have any side-effect, and will simply return dummy values.", 219 | vec![ 220 | Arg::Plain { 221 | name: "file", 222 | required: true, 223 | help: "Input wasm file.", 224 | }, 225 | Arg::KeyValue { 226 | keys: &["-o", "--output"], 227 | value_type: "PATH", 228 | help: "Specify the output path.", 229 | }, 230 | Arg::KeyValue { 231 | keys: &["--stub-module"], 232 | value_type: "STRING", 233 | help: "Stub the given module. 234 | You can also give a list of comma-separated modules.", 235 | }, 236 | Arg::KeyValue { 237 | keys: &["--stub-function"], 238 | value_type: "STRING:STRING", 239 | help: "Stub the given function. It must have the format 'module:function'. 240 | Example: 241 | wasi-stub input.wasm --stub-function horrible_module:terrible_function 242 | 243 | Multiple functions can be given: simply separate them with commas (without whitespace).", 244 | }, 245 | Arg::KeyValue { 246 | keys: &["-r", "--return-value"], 247 | value_type: "INTEGER", 248 | help: "Make all stubbed function that return values return this number. By default, functions return 76." 249 | }, 250 | Arg::LongFlag { 251 | name: "--list", 252 | help: "List the functions to stub, but don't write anything.", 253 | }, 254 | ], 255 | ); 256 | 257 | arg_parser.parse().map_err(Error::message)?; 258 | 259 | if arg_parser.requested_help { 260 | arg_parser.print_help_message(); 261 | std::process::exit(0); 262 | } 263 | 264 | let path = PathBuf::from(&arg_parser.plain_args["file"]); 265 | let list = arg_parser.long_flags.contains("--list"); 266 | let mut output_path = None; 267 | let mut should_stub = ShouldStub::default(); 268 | let mut return_value: u32 = 76; 269 | 270 | if let Some(path) = arg_parser 271 | .key_values 272 | .get("--output") 273 | .or(arg_parser.key_values.get("-o")) 274 | { 275 | output_path = Some(PathBuf::from(path)); 276 | } 277 | if let Some(stub_functions) = arg_parser.key_values.get("--stub-function") { 278 | if let Some(stub_functions) = stub_functions.to_str() { 279 | for function in stub_functions.split(',') { 280 | let (module, function) = match function.split_once(':') { 281 | Some((m, f)) => (m, f), 282 | None => { 283 | return Err(Error::message(format!("Malformed argument: {function}"))) 284 | } 285 | }; 286 | let functions = should_stub 287 | .modules 288 | .entry(module.to_owned()) 289 | .or_insert(FunctionsToStub::Some(HashSet::new())); 290 | match functions { 291 | FunctionsToStub::All => {} 292 | FunctionsToStub::Some(set) => { 293 | set.insert(function.to_owned()); 294 | } 295 | } 296 | } 297 | } 298 | } 299 | if let Some(stub_modules) = arg_parser.key_values.get("--stub-module") { 300 | if let Some(stub_modules) = stub_modules.to_str() { 301 | for module in stub_modules.split(',') { 302 | should_stub 303 | .modules 304 | .insert(module.to_owned(), FunctionsToStub::All); 305 | } 306 | } 307 | } 308 | if let Some(value) = arg_parser 309 | .key_values 310 | .get("--return-value") 311 | .or(arg_parser.key_values.get("-r")) 312 | { 313 | match value.to_str() { 314 | Some(v) => return_value = v.parse()?, 315 | None => return Err(Error::message(format!("Invalid number: {value:?}"))), 316 | } 317 | } 318 | 319 | Ok(Self { 320 | binary: std::fs::read(&path)?, 321 | path, 322 | output_path, 323 | list, 324 | should_stub, 325 | return_value, 326 | }) 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /examples/hello_c/.gitignore: -------------------------------------------------------------------------------- 1 | /hello.wasm 2 | /hello.pdf 3 | -------------------------------------------------------------------------------- /examples/hello_c/README.md: -------------------------------------------------------------------------------- 1 | # C wasm plugin example 2 | 3 | This is a bare-bone typst plugin, written in C. 4 | 5 | ## Compile 6 | 7 | To compile this example, you need the [emcc compiler](https://emscripten.org/docs/getting_started/downloads.html). Then, run the command 8 | 9 | ```sh 10 | emcc --no-entry -O3 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -o hello.wasm hello.c 11 | ``` 12 | 13 | Emcc always build with WASI, so we need to stub WASI functions: 14 | 15 | ```sh 16 | pushd ../../wasi-stub 17 | cargo run -- ../examples/hello_c/hello.wasm -o ../examples/hello_c/hello.wasm 18 | popd 19 | ``` 20 | 21 | ## Build with typst 22 | 23 | Simply run `typst compile hello.typ`, and observe that it works ! -------------------------------------------------------------------------------- /examples/hello_c/hello.c: -------------------------------------------------------------------------------- 1 | #include "emscripten.h" 2 | 3 | #ifdef __cplusplus 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define PROTOCOL_FUNCTION __attribute__((import_module("typst_env"))) extern "C" 9 | #else 10 | #include 11 | #include 12 | #include 13 | #include 14 | #define PROTOCOL_FUNCTION __attribute__((import_module("typst_env"))) extern 15 | #endif 16 | 17 | // === 18 | // Functions for the protocol 19 | 20 | PROTOCOL_FUNCTION void 21 | wasm_minimal_protocol_send_result_to_host(const uint8_t *ptr, size_t len); 22 | PROTOCOL_FUNCTION void wasm_minimal_protocol_write_args_to_buffer(uint8_t *ptr); 23 | 24 | // === 25 | 26 | EMSCRIPTEN_KEEPALIVE 27 | int32_t hello(void) { 28 | const char message[] = "Hello from wasm!!!"; 29 | const size_t length = sizeof(message); 30 | wasm_minimal_protocol_send_result_to_host((const uint8_t *)message, length - 1); 31 | return 0; 32 | } 33 | 34 | EMSCRIPTEN_KEEPALIVE 35 | int32_t double_it(size_t arg_len) { 36 | size_t result_len = arg_len * 2; 37 | uint8_t *result = (uint8_t *)malloc(result_len); 38 | if (result == NULL) { 39 | return 1; 40 | } 41 | wasm_minimal_protocol_write_args_to_buffer(result); 42 | memcpy(result + arg_len, result, arg_len); 43 | wasm_minimal_protocol_send_result_to_host(result, result_len); 44 | free(result); 45 | return 0; 46 | } 47 | 48 | EMSCRIPTEN_KEEPALIVE 49 | int32_t concatenate(size_t arg1_len, size_t arg2_len) { 50 | size_t total_len = arg1_len + arg2_len; 51 | uint8_t *args = (uint8_t *)malloc(total_len); 52 | uint8_t *result = (uint8_t *)malloc(total_len + 1); 53 | if (args == NULL) { 54 | return 1; 55 | } else if (result == NULL) { 56 | free(args); 57 | return 1; 58 | } 59 | 60 | wasm_minimal_protocol_write_args_to_buffer(args); 61 | uint8_t *arg1 = args; 62 | uint8_t *arg2 = args + arg1_len; 63 | 64 | memcpy(result, arg1, arg1_len); 65 | result[arg1_len] = '*'; 66 | memcpy(result + arg1_len + 1, arg2, arg2_len); 67 | 68 | wasm_minimal_protocol_send_result_to_host(result, total_len + 1); 69 | 70 | free(result); 71 | free(args); 72 | return 0; 73 | } 74 | 75 | EMSCRIPTEN_KEEPALIVE 76 | int32_t shuffle(size_t arg1_len, size_t arg2_len, size_t arg3_len) { 77 | size_t result_len = arg1_len + arg2_len + arg3_len + 2; 78 | uint8_t *args = (uint8_t *)malloc(arg1_len + arg2_len + arg3_len); 79 | uint8_t *result = (uint8_t *)malloc(result_len); 80 | if (args == NULL) { 81 | return 1; 82 | } else if (result == NULL) { 83 | free(args); 84 | return 1; 85 | } 86 | 87 | wasm_minimal_protocol_write_args_to_buffer(args); 88 | uint8_t *arg1 = args; 89 | uint8_t *arg2 = args + arg1_len; 90 | uint8_t *arg3 = args + arg1_len + arg2_len; 91 | 92 | memcpy(result, arg3, arg3_len); 93 | result[arg3_len] = '-'; 94 | memcpy(result + arg3_len + 1, arg1, arg1_len); 95 | result[arg3_len + 1 + arg1_len] = '-'; 96 | memcpy(result + arg3_len + 1 + arg1_len + 1, arg2, arg2_len); 97 | 98 | wasm_minimal_protocol_send_result_to_host(result, result_len); 99 | 100 | free(result); 101 | free(args); 102 | return 0; 103 | } 104 | 105 | EMSCRIPTEN_KEEPALIVE 106 | int32_t returns_ok() { 107 | const char message[] = "This is an `Ok`"; 108 | const size_t length = sizeof(message); 109 | wasm_minimal_protocol_send_result_to_host((const uint8_t *)message, length - 1); 110 | return 0; 111 | } 112 | 113 | EMSCRIPTEN_KEEPALIVE 114 | int32_t returns_err() { 115 | const char message[] = "This is an `Err`"; 116 | const size_t length = sizeof(message); 117 | wasm_minimal_protocol_send_result_to_host((const uint8_t *)message, length - 1); 118 | return 1; 119 | } 120 | 121 | EMSCRIPTEN_KEEPALIVE 122 | int32_t will_panic() { 123 | exit(1); 124 | } 125 | -------------------------------------------------------------------------------- /examples/hello_c/hello.typ: -------------------------------------------------------------------------------- 1 | #{ 2 | let p = plugin("./hello.wasm") 3 | 4 | assert.eq(str(p.hello()), "Hello from wasm!!!") 5 | assert.eq(str(p.double_it(bytes("abc"))), "abcabc") 6 | assert.eq(str(p.concatenate(bytes("hello"), bytes("world"))), "hello*world") 7 | assert.eq(str(p.shuffle(bytes("s1"), bytes("s2"), bytes("s3"))), "s3-s1-s2") 8 | assert.eq(str(p.returns_ok()), "This is an `Ok`") 9 | // p.will_panic() // Fails compilation 10 | // p.returns_err() // Fails compilation with an error message 11 | } 12 | -------------------------------------------------------------------------------- /examples/hello_rust/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /hello.pdf 3 | /hello.wasm 4 | /hello-wasi.wasm -------------------------------------------------------------------------------- /examples/hello_rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 10 | 11 | [[package]] 12 | name = "ciborium" 13 | version = "0.2.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 16 | dependencies = [ 17 | "ciborium-io", 18 | "ciborium-ll", 19 | "serde", 20 | ] 21 | 22 | [[package]] 23 | name = "ciborium-io" 24 | version = "0.2.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 27 | 28 | [[package]] 29 | name = "ciborium-ll" 30 | version = "0.2.2" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 33 | dependencies = [ 34 | "ciborium-io", 35 | "half", 36 | ] 37 | 38 | [[package]] 39 | name = "crunchy" 40 | version = "0.2.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 43 | 44 | [[package]] 45 | name = "half" 46 | version = "2.5.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" 49 | dependencies = [ 50 | "cfg-if", 51 | "crunchy", 52 | ] 53 | 54 | [[package]] 55 | name = "hello" 56 | version = "0.1.0" 57 | dependencies = [ 58 | "ciborium", 59 | "serde", 60 | "wasm-minimal-protocol", 61 | ] 62 | 63 | [[package]] 64 | name = "proc-macro2" 65 | version = "1.0.60" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" 68 | dependencies = [ 69 | "unicode-ident", 70 | ] 71 | 72 | [[package]] 73 | name = "quote" 74 | version = "1.0.28" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 77 | dependencies = [ 78 | "proc-macro2", 79 | ] 80 | 81 | [[package]] 82 | name = "serde" 83 | version = "1.0.164" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" 86 | dependencies = [ 87 | "serde_derive", 88 | ] 89 | 90 | [[package]] 91 | name = "serde_derive" 92 | version = "1.0.164" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" 95 | dependencies = [ 96 | "proc-macro2", 97 | "quote", 98 | "syn", 99 | ] 100 | 101 | [[package]] 102 | name = "syn" 103 | version = "2.0.20" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "fcb8d4cebc40aa517dfb69618fa647a346562e67228e2236ae0042ee6ac14775" 106 | dependencies = [ 107 | "proc-macro2", 108 | "quote", 109 | "unicode-ident", 110 | ] 111 | 112 | [[package]] 113 | name = "unicode-ident" 114 | version = "1.0.9" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 117 | 118 | [[package]] 119 | name = "venial" 120 | version = "0.5.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" 123 | dependencies = [ 124 | "proc-macro2", 125 | "quote", 126 | ] 127 | 128 | [[package]] 129 | name = "wasm-minimal-protocol" 130 | version = "0.1.0" 131 | dependencies = [ 132 | "proc-macro2", 133 | "quote", 134 | "venial", 135 | ] 136 | -------------------------------------------------------------------------------- /examples/hello_rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | # Same package and version as Typst uses. 11 | ciborium = "0.2.1" 12 | serde = "1.0" 13 | wasm-minimal-protocol = { path = "../../crates/macro" } 14 | 15 | 16 | [profile.release] 17 | lto = true # Enable link-time optimization 18 | strip = true # Strip symbols from binary* 19 | opt-level = 'z' # Optimize for size 20 | codegen-units = 1 # Reduce number of codegen units to increase optimizations 21 | panic = 'abort' # Abort on panic 22 | 23 | [workspace] # so that it is not included in the upper workspace 24 | -------------------------------------------------------------------------------- /examples/hello_rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust wasm plugin example 2 | 3 | This is a bare-bone typst plugin, written in Rust. It uses the [wasm-minimal-protocol](../../) crate to easily define plugin functions. 4 | 5 | ## Compile 6 | 7 | To compile this example, you need to have a working [Rust toolchain](https://www.rust-lang.org/). Then we need to install the `wasm32-unknown-unknown` target: 8 | 9 | ```sh 10 | rustup target add wasm32-unknown-unknown 11 | ``` 12 | 13 | Then, build the crate with this target: 14 | 15 | ```sh 16 | cargo build --release --target wasm32-unknown-unknown 17 | cp ./target/wasm32-unknown-unknown/release/hello.wasm ./ 18 | ``` 19 | 20 | ## Compile with wasi 21 | 22 | If you want to build with WASI, use the `wasm32-wasip1` target: 23 | 24 | ```sh 25 | rustup target add wasm32-wasip1 26 | cargo build --release --target wasm32-wasip1 27 | cp ./target/wasm32-wasip1/release/hello.wasm ./ 28 | ``` 29 | 30 | Then, stub the resulting binary: 31 | 32 | ```sh 33 | cargo run --manifest-path ../../crates/wasi-stub/Cargo.toml hello.wasm -o hello.wasm 34 | ``` 35 | 36 | ## Build with typst 37 | 38 | Simply run `typst compile hello.typ`, and observe that it works ! 39 | -------------------------------------------------------------------------------- /examples/hello_rust/hello.typ: -------------------------------------------------------------------------------- 1 | #{ 2 | let p = plugin("./hello.wasm") 3 | 4 | assert.eq(str(p.hello()), "Hello from wasm!!!") 5 | assert.eq(str(p.double_it(bytes("abc"))), "abcabc") 6 | assert.eq(str(p.concatenate(bytes("hello"), bytes("world"))), "hello*world") 7 | assert.eq(str(p.shuffle(bytes("s1"), bytes("s2"), bytes("s3"))), "s3-s1-s2") 8 | assert.eq(str(p.returns_ok()), "This is an `Ok`") 9 | assert.eq(str(p.set_to_a(bytes("xxxyyz"))), "aaaaaa") 10 | assert.eq(str(p.set_to_a_reuse_buffer(bytes("xxxyyz"))), "aaaaaa") 11 | // p.will_panic() // Fails compilation 12 | // p.returns_err() // Fails compilation with an error message 13 | 14 | let encoded = cbor.encode((x: 1, y: 2.0)) 15 | let decoded = cbor(p.complex_data(encoded)) 16 | assert.eq(decoded, 3.0) 17 | } 18 | -------------------------------------------------------------------------------- /examples/hello_rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_minimal_protocol::*; 2 | 3 | // Only necessary when using cbor for passing arguments. 4 | use ciborium::{de::from_reader, ser::into_writer}; 5 | 6 | initiate_protocol!(); 7 | 8 | #[wasm_func] 9 | pub fn hello() -> Vec { 10 | b"Hello from wasm!!!".to_vec() 11 | } 12 | 13 | #[wasm_func] 14 | pub fn double_it(arg: &[u8]) -> Vec { 15 | [arg, arg].concat() 16 | } 17 | 18 | #[wasm_func] 19 | pub fn concatenate(arg1: &[u8], arg2: &[u8]) -> Vec { 20 | [arg1, b"*", arg2].concat() 21 | } 22 | 23 | #[wasm_func] 24 | pub fn shuffle(arg1: &[u8], arg2: &[u8], arg3: &[u8]) -> Vec { 25 | [arg3, b"-", arg1, b"-", arg2].concat() 26 | } 27 | 28 | #[wasm_func] 29 | pub fn returns_ok() -> Result, String> { 30 | Ok(b"This is an `Ok`".to_vec()) 31 | } 32 | 33 | #[wasm_func] 34 | pub fn returns_err() -> Result, String> { 35 | Err(String::from("This is an `Err`")) 36 | } 37 | 38 | #[wasm_func] 39 | pub fn will_panic() -> Vec { 40 | panic!("unconditional panic") 41 | } 42 | 43 | #[derive(serde::Deserialize)] 44 | struct ComplexDataArgs { 45 | x: i32, 46 | y: f64, 47 | } 48 | 49 | #[wasm_func] 50 | pub fn complex_data(arg: &[u8]) -> Vec { 51 | let args: ComplexDataArgs = from_reader(arg).unwrap(); 52 | let sum = args.x as f64 + args.y; 53 | let mut out = Vec::new(); 54 | into_writer(&sum, &mut out).unwrap(); 55 | out 56 | } 57 | 58 | #[wasm_func] 59 | pub fn set_to_a(arg: &mut [u8]) -> Vec { 60 | for c in &mut *arg { 61 | *c = b'a'; 62 | } 63 | arg.to_vec() 64 | } 65 | 66 | #[wasm_func] 67 | pub fn set_to_a_reuse_buffer(arg: &mut [u8]) -> &[u8] { 68 | for c in &mut *arg { 69 | *c = b'a'; 70 | } 71 | arg 72 | } 73 | -------------------------------------------------------------------------------- /examples/hello_zig/.gitignore: -------------------------------------------------------------------------------- 1 | /hello.wasm 2 | /hello.wasm.o 3 | /hello-wasi.wasm 4 | /hello-wasi.wasm.o 5 | /hello.pdf -------------------------------------------------------------------------------- /examples/hello_zig/README.md: -------------------------------------------------------------------------------- 1 | # Zig wasm plugin example 2 | 3 | This is a bare-bone typst plugin, written in Zig. 4 | 5 | ## Compile 6 | 7 | To compile this example, you need the [zig compiler](https://ziglang.org/learn/getting-started/#installing-zig). Then, run the command 8 | 9 | ```sh 10 | zig build-exe hello.zig -target wasm32-freestanding -fno-entry -O ReleaseSmall \ 11 | --export=hello \ 12 | --export=double_it \ 13 | --export=concatenate \ 14 | --export=shuffle \ 15 | --export=returns_ok \ 16 | --export=returns_err \ 17 | --export=will_panic 18 | ``` 19 | 20 | ## Compile with wasi 21 | 22 | If you want to build with WASI, use the `wasm32-wasi` target: 23 | 24 | ```sh 25 | zig build-exe hello.zig -target wasm32-wasi -fno-entry -O ReleaseSmall \ 26 | --export=hello \ 27 | --export=double_it \ 28 | --export=concatenate \ 29 | --export=shuffle \ 30 | --export=returns_ok \ 31 | --export=returns_err \ 32 | --export=will_panic 33 | ``` 34 | 35 | Then, stub the resulting binary: 36 | 37 | ```sh 38 | cargo run --manifest-path ../../crates/wasi-stub/Cargo.toml hello.wasm -o hello.wasm 39 | ``` 40 | 41 | ## Build with typst 42 | 43 | Simply run `typst compile hello.typ`, and observe that it works ! 44 | -------------------------------------------------------------------------------- /examples/hello_zig/hello.typ: -------------------------------------------------------------------------------- 1 | #{ 2 | let p = plugin("./hello.wasm") 3 | 4 | assert.eq(str(p.hello()), "Hello from wasm!!!") 5 | assert.eq(str(p.double_it(bytes("abc"))), "abcabc") 6 | assert.eq(str(p.concatenate(bytes("hello"), bytes("world"))), "hello*world") 7 | assert.eq(str(p.shuffle(bytes("s1"), bytes("s2"), bytes("s3"))), "s3-s1-s2") 8 | assert.eq(str(p.returns_ok()), "This is an `Ok`") 9 | // p.will_panic() // Fails compilation 10 | // p.returns_err() // Fails compilation with an error message 11 | } 12 | -------------------------------------------------------------------------------- /examples/hello_zig/hello.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const allocator = std.heap.page_allocator; 3 | 4 | // === 5 | // Functions for the protocol 6 | 7 | extern "typst_env" fn wasm_minimal_protocol_send_result_to_host(ptr: [*]const u8, len: usize) void; 8 | extern "typst_env" fn wasm_minimal_protocol_write_args_to_buffer(ptr: [*]u8) void; 9 | 10 | // === 11 | 12 | export fn hello() i32 { 13 | const message = "Hello from wasm!!!"; 14 | wasm_minimal_protocol_send_result_to_host(message.ptr, message.len); 15 | return 0; 16 | } 17 | 18 | export fn double_it(arg1_len: usize) i32 { 19 | var result = allocator.alloc(u8, arg1_len * 2) catch return 1; 20 | defer allocator.free(result); 21 | wasm_minimal_protocol_write_args_to_buffer(result.ptr); 22 | for (0..arg1_len) |i| { 23 | result[i + arg1_len] = result[i]; 24 | } 25 | wasm_minimal_protocol_send_result_to_host(result.ptr, result.len); 26 | return 0; 27 | } 28 | 29 | export fn concatenate(arg1_len: usize, arg2_len: usize) i32 { 30 | const args = allocator.alloc(u8, arg1_len + arg2_len) catch return 1; 31 | defer allocator.free(args); 32 | wasm_minimal_protocol_write_args_to_buffer(args.ptr); 33 | 34 | var result = allocator.alloc(u8, arg1_len + arg2_len + 1) catch return 1; 35 | defer allocator.free(result); 36 | for (0..arg1_len) |i| { 37 | result[i] = args[i]; 38 | } 39 | result[arg1_len] = '*'; 40 | for (arg1_len..arg1_len + arg2_len) |i| { 41 | result[i + 1] = args[i]; 42 | } 43 | wasm_minimal_protocol_send_result_to_host(result.ptr, result.len); 44 | return 0; 45 | } 46 | 47 | export fn shuffle(arg1_len: usize, arg2_len: usize, arg3_len: usize) i32 { 48 | const args_len = arg1_len + arg2_len + arg3_len; 49 | var args = allocator.alloc(u8, args_len) catch return 1; 50 | defer allocator.free(args); 51 | wasm_minimal_protocol_write_args_to_buffer(args.ptr); 52 | 53 | const arg1 = args[0..arg1_len]; 54 | const arg2 = args[arg1_len .. arg1_len + arg2_len]; 55 | const arg3 = args[arg1_len + arg2_len .. args.len]; 56 | 57 | var result = allocator.alloc(u8, arg1_len + arg2_len + arg3_len + 2) catch return 1; 58 | defer allocator.free(result); 59 | @memcpy(result[0..arg3.len], arg3); 60 | result[arg3.len] = '-'; 61 | @memcpy(result[arg3.len + 1 ..][0..arg1.len], arg1); 62 | result[arg3.len + arg1.len + 1] = '-'; 63 | @memcpy(result[arg3.len + arg1.len + 2 ..][0..arg2.len], arg2); 64 | 65 | wasm_minimal_protocol_send_result_to_host(result.ptr, result.len); 66 | return 0; 67 | } 68 | 69 | export fn returns_ok() i32 { 70 | const message = "This is an `Ok`"; 71 | wasm_minimal_protocol_send_result_to_host(message.ptr, message.len); 72 | return 0; 73 | } 74 | 75 | export fn returns_err() i32 { 76 | const message = "This is an `Err`"; 77 | wasm_minimal_protocol_send_result_to_host(message.ptr, message.len); 78 | return 1; 79 | } 80 | 81 | export fn will_panic() i32 { 82 | std.debug.panic("Panicking, this message will not be seen...", .{}); 83 | } 84 | --------------------------------------------------------------------------------