├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE-APACHE-2.0 ├── LICENSE-MIT ├── README.md ├── benches └── bench.rs ├── build.rs ├── ci ├── cargo-save ├── cargo-wasi ├── run-saved-jobs └── wasi.mjs ├── clippy.toml ├── src ├── api.rs ├── backend │ ├── emscripten.rs │ ├── itanium.rs │ ├── mod.rs │ ├── panic.rs │ ├── seh.rs │ └── unimplemented.rs ├── heterogeneous_stack │ ├── align.rs │ ├── array.rs │ ├── heap.rs │ ├── mod.rs │ └── unbounded.rs ├── intrinsic.rs ├── lib.rs └── stacked_exceptions.rs └── valgrind.supp /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | linux-native: 12 | timeout-minutes: 3 13 | runs-on: ${{ matrix.machine.os }} 14 | if: success() || failure() 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | libc: [gnu, musl] 19 | machine: 20 | - os: ubuntu-latest 21 | arch: x86_64 22 | - os: ubuntu-24.04-arm 23 | arch: aarch64 24 | env: 25 | target: ${{ matrix.machine.arch }}-unknown-linux-${{ matrix.libc }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Install Rust 30 | run: rustup update nightly && rustup default nightly 31 | - name: Add target 32 | run: rustup target add $target 33 | - name: Test with panic backend (debug) 34 | run: LITHIUM_BACKEND=panic cargo test --target $target 35 | - name: Test with Itanium backend (debug) 36 | run: LITHIUM_BACKEND=itanium cargo test --target $target 37 | - name: Test with std thread locals (debug) 38 | run: LITHIUM_THREAD_LOCAL=std cargo test --target $target 39 | - name: Test with panic backend (release) 40 | run: LITHIUM_BACKEND=panic cargo test --target $target --release 41 | - name: Test with Itanium backend (release) 42 | run: LITHIUM_BACKEND=itanium cargo test --target $target --release 43 | - name: Test with std thread locals (release) 44 | run: LITHIUM_THREAD_LOCAL=std cargo test --target $target --release 45 | 46 | linux-cross: 47 | timeout-minutes: 5 48 | runs-on: ubuntu-latest 49 | if: success() || failure() 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | target: 54 | - aarch64-linux-android 55 | - arm-linux-androideabi 56 | - armv7-linux-androideabi 57 | - arm-unknown-linux-gnueabi 58 | - arm-unknown-linux-gnueabihf 59 | - arm-unknown-linux-musleabi 60 | - arm-unknown-linux-musleabihf 61 | - armv7-unknown-linux-gnueabihf 62 | - i686-unknown-linux-gnu 63 | - i686-unknown-linux-musl 64 | - loongarch64-unknown-linux-gnu 65 | - loongarch64-unknown-linux-musl 66 | - mips64-unknown-linux-gnuabi64 67 | - mips64el-unknown-linux-gnuabi64 68 | - powerpc64-unknown-linux-gnu 69 | - powerpc64le-unknown-linux-gnu 70 | - riscv64gc-unknown-linux-gnu 71 | - s390x-unknown-linux-gnu 72 | - sparc64-unknown-linux-gnu 73 | env: 74 | target: ${{ matrix.target }} 75 | steps: 76 | - name: Checkout 77 | uses: actions/checkout@v4 78 | - name: Install Rust 79 | run: rustup update nightly && rustup default nightly 80 | - name: Install Cross 81 | run: cargo install cross --git https://github.com/cross-rs/cross 82 | - name: Test with panic backend (debug) 83 | run: LITHIUM_BACKEND=panic cross test --target $target 84 | - name: Test with Itanium backend (debug) 85 | run: LITHIUM_BACKEND=itanium cross test --target $target 86 | - name: Test with std thread locals (debug) 87 | run: LITHIUM_THREAD_LOCAL=std cross test --target $target 88 | - name: Test with panic backend (release) 89 | run: LITHIUM_BACKEND=panic cross test --target $target --release 90 | - name: Test with Itanium backend (release) 91 | run: LITHIUM_BACKEND=itanium cross test --target $target --release 92 | - name: Test with std thread locals (release) 93 | run: LITHIUM_THREAD_LOCAL=std cross test --target $target --release 94 | 95 | emscripten: 96 | timeout-minutes: 5 97 | runs-on: ubuntu-latest 98 | if: success() || failure() 99 | env: 100 | target: wasm32-unknown-emscripten 101 | steps: 102 | - name: Checkout 103 | uses: actions/checkout@v4 104 | - name: Install Rust 105 | run: rustup update nightly && rustup default nightly 106 | - name: Install Cross 107 | run: cargo install cross --git https://github.com/cross-rs/cross 108 | - name: Test with panic backend (debug) 109 | run: LITHIUM_BACKEND=panic cross test --target $target 110 | - name: Test with Itanium backend (debug) 111 | run: RUSTFLAGS="-Z emscripten_wasm_eh" LITHIUM_BACKEND=itanium cross test --target $target -Z build-std 112 | - name: Test with Emscripten backend (debug) 113 | run: LITHIUM_BACKEND=emscripten cross test --target $target 114 | - name: Test with std thread locals (debug) 115 | run: LITHIUM_THREAD_LOCAL=std cross test --target $target 116 | - name: Test with panic backend (release) 117 | run: LITHIUM_BACKEND=panic cross test --target $target --release 118 | # XXX: https://github.com/rust-lang/rust/issues/132416 119 | # - name: Test with Itanium backend (release) 120 | # run: RUSTFLAGS="-Z emscripten_wasm_eh" LITHIUM_BACKEND=itanium cross test --target $target --release -Z build-std 121 | - name: Test with Emscripten backend (release) 122 | run: LITHIUM_BACKEND=emscripten cross test --target $target --release 123 | - name: Test with std thread locals (release) 124 | run: LITHIUM_THREAD_LOCAL=std cross test --target $target --release 125 | 126 | wasi: 127 | timeout-minutes: 3 128 | runs-on: ubuntu-latest 129 | if: success() || failure() 130 | env: 131 | target: wasm32-wasip1 132 | steps: 133 | - name: Checkout 134 | uses: actions/checkout@v4 135 | - name: Install NodeJS 136 | uses: actions/setup-node@v4 137 | with: 138 | node-version: 22 139 | - name: Install Rust 140 | run: rustup update nightly && rustup default nightly 141 | - name: Add target 142 | run: rustup target add $target 143 | - name: Install rust-src 144 | run: rustup component add rust-src 145 | - name: Test with panic backend (debug) 146 | run: LITHIUM_BACKEND=panic ci/cargo-wasi test --target $target 147 | - name: Test with Itanium backend (debug) 148 | run: LITHIUM_BACKEND=itanium ci/cargo-wasi test --target $target 149 | - name: Test with std thread locals (debug) 150 | run: LITHIUM_THREAD_LOCAL=std ci/cargo-wasi test --target $target 151 | - name: Test with panic backend (release) 152 | run: LITHIUM_BACKEND=panic ci/cargo-wasi test --target $target --release 153 | # XXX: Upstream bug at https://github.com/rust-lang/rust/issues/132416 154 | # - name: Test with Itanium backend (release) 155 | # run: LITHIUM_BACKEND=itanium ci/cargo-wasi test --target $target --release 156 | # - name: Test with std thread locals (release) 157 | # run: LITHIUM_THREAD_LOCAL=std ci/cargo-wasi test --target $target --release 158 | 159 | darwin: 160 | timeout-minutes: 3 161 | runs-on: ${{ matrix.os }} 162 | if: success() || failure() 163 | strategy: 164 | fail-fast: false 165 | matrix: 166 | os: 167 | - macos-13 # x86_64 168 | - macos-15 # aarch64 169 | steps: 170 | - name: Checkout 171 | uses: actions/checkout@v4 172 | - name: Install Rust 173 | run: rustup update nightly && rustup default nightly 174 | - name: Test with panic backend (debug) 175 | run: LITHIUM_BACKEND=panic cargo test 176 | - name: Test with Itanium backend (debug) 177 | run: LITHIUM_BACKEND=itanium cargo test 178 | - name: Test with std thread locals (debug) 179 | run: LITHIUM_THREAD_LOCAL=std cargo test 180 | - name: Test with panic backend (release) 181 | run: LITHIUM_BACKEND=panic cargo test --release 182 | - name: Test with Itanium backend (release) 183 | run: LITHIUM_BACKEND=itanium cargo test --release 184 | - name: Test with std thread locals (release) 185 | run: LITHIUM_THREAD_LOCAL=std cargo test --release 186 | 187 | windows: 188 | timeout-minutes: 5 189 | runs-on: windows-latest 190 | if: success() || failure() 191 | strategy: 192 | fail-fast: false 193 | matrix: 194 | arch: [x86_64, i686] 195 | abi: [msvc, gnu, gnullvm] 196 | env: 197 | host: ${{ matrix.arch }}-pc-windows-${{ matrix.abi == 'gnullvm' && 'gnu' || matrix.abi }} 198 | target: ${{ matrix.arch }}-pc-windows-${{ matrix.abi }} 199 | defaults: 200 | run: 201 | shell: bash 202 | steps: 203 | - name: Checkout 204 | uses: actions/checkout@v4 205 | - name: Set default-host 206 | run: rustup set default-host $host 207 | - name: Install Rust 208 | run: rustup update nightly && rustup default nightly 209 | - name: Install and configure LLVM-MinGW 210 | if: matrix.abi == 'gnullvm' 211 | run: > 212 | rustup target add $target && 213 | curl -L https://github.com/mstorsjo/llvm-mingw/releases/download/20250114/llvm-mingw-20250114-ucrt-x86_64.zip -o llvm-mingw.zip && 214 | 7z x llvm-mingw.zip && 215 | echo "[target.${{ matrix.arch }}-pc-windows-gnullvm]" >~/.cargo/config.toml && 216 | echo "linker = '$(pwd -W)/llvm-mingw-20250114-ucrt-x86_64/bin/clang'" >>~/.cargo/config.toml && 217 | echo "rustflags = ['-C', 'target-feature=+crt-static'${{ matrix.arch == 'i686' && ', ''-C'', ''link-args=-m32''' || '' }}]" >>~/.cargo/config.toml 218 | - name: Test with panic backend (debug) 219 | run: LITHIUM_BACKEND=panic ci/cargo-save "Test with panic backend (debug)" test --target $target 220 | - name: Test with SEH backend (debug) 221 | if: matrix.abi == 'msvc' 222 | run: LITHIUM_BACKEND=seh ci/cargo-save "Test with SEH backend (debug)" test --target $target 223 | - name: Test with Itanium backend (debug) 224 | if: matrix.abi == 'gnu' || matrix.abi == 'gnullvm' 225 | run: LITHIUM_BACKEND=itanium ci/cargo-save "Test with Itanium backend (debug)" test --target $target 226 | - name: Test with std thread locals (debug) 227 | if: matrix.abi == 'msvc' || matrix.abi == 'gnullvm' 228 | run: LITHIUM_THREAD_LOCAL=std ci/cargo-save "Test with std thread locals (debug)" test --target $target 229 | - name: Test with panic backend (release) 230 | run: LITHIUM_BACKEND=panic ci/cargo-save "Test with panic backend (release)" test --target $target --release 231 | - name: Test with SEH backend (release) 232 | if: matrix.abi == 'msvc' 233 | run: LITHIUM_BACKEND=seh ci/cargo-save "Test with SEH backend (release)" test --target $target --release 234 | - name: Test with Itanium backend (release) 235 | if: matrix.abi == 'gnu' || matrix.abi == 'gnullvm' 236 | run: LITHIUM_BACKEND=itanium ci/cargo-save "Test with Itanium backend (release)" test --target $target --release 237 | - name: Test with std thread locals (release) 238 | if: matrix.abi == 'msvc' || matrix.abi == 'gnullvm' 239 | run: LITHIUM_THREAD_LOCAL=std ci/cargo-save "Test with std thread locals (release)" test --target $target --release 240 | - name: Upload built tests for Wine 241 | uses: actions/upload-artifact@v4 242 | with: 243 | name: tests-${{ env.target }} 244 | path: saved-jobs 245 | retention-days: 1 246 | 247 | wine: 248 | timeout-minutes: 3 249 | runs-on: ${{ matrix.machine.os }} 250 | needs: windows 251 | strategy: 252 | fail-fast: false 253 | matrix: 254 | machine: 255 | - os: ubuntu-latest 256 | ubuntu_arch: amd64 257 | rust_arch: x86_64 258 | - os: ubuntu-latest 259 | ubuntu_arch: i386 260 | rust_arch: i686 261 | # XXX: We should eventually enable 'gnullvm', too. Itanium under gnullvm is currently broken 262 | # because Wine does not currently align module thread locals correctly, and we rely on that. 263 | # https://bugs.winehq.org/show_bug.cgi?id=57700 264 | abi: [msvc, gnu] 265 | env: 266 | WINEDEBUG: fixme+all,err+all # :ferrisClueless: 267 | target: ${{ matrix.machine.rust_arch }}-pc-windows-${{ matrix.abi }} 268 | steps: 269 | - name: Checkout 270 | uses: actions/checkout@v4 271 | - name: Install wine 272 | run: | 273 | set -exuo pipefail 274 | sudo dpkg --add-architecture ${{ matrix.machine.ubuntu_arch }} 275 | sudo apt-get update 276 | sudo apt install wine:${{ matrix.machine.ubuntu_arch }} 277 | wineboot 278 | - name: Download built tests 279 | uses: actions/download-artifact@v4 280 | with: 281 | name: tests-${{ env.target }} 282 | path: saved-jobs 283 | - name: Run tests 284 | run: ci/run-saved-jobs 285 | 286 | miri-linux: 287 | timeout-minutes: 5 288 | runs-on: ubuntu-latest 289 | if: success() || failure() 290 | strategy: 291 | fail-fast: false 292 | matrix: 293 | target: 294 | - x86_64-unknown-linux-gnu 295 | - i686-unknown-linux-gnu 296 | - powerpc64-unknown-linux-gnu 297 | env: 298 | target: ${{ matrix.target }} 299 | steps: 300 | - name: Checkout 301 | uses: actions/checkout@v4 302 | - name: Install Rust 303 | run: rustup update nightly && rustup default nightly 304 | - name: Install Miri 305 | run: rustup component add miri 306 | - name: Add target 307 | run: rustup target add $target 308 | - name: Test with panic backend 309 | run: LITHIUM_BACKEND=panic cargo miri test --target $target 310 | - name: Test with Itanium backend 311 | run: LITHIUM_BACKEND=itanium cargo miri test --target $target 312 | - name: Test with std thread locals 313 | run: LITHIUM_THREAD_LOCAL=std cargo miri test --target $target 314 | 315 | valgrind: 316 | timeout-minutes: 5 317 | runs-on: ubuntu-latest 318 | env: 319 | VALGRINDFLAGS: --suppressions=valgrind.supp 320 | if: success() || failure() 321 | steps: 322 | - name: Checkout 323 | uses: actions/checkout@v4 324 | - name: Install Rust 325 | run: rustup update nightly && rustup default nightly 326 | - name: Install cargo valgrind 327 | run: sudo apt-get update && sudo apt-get install valgrind && cargo install cargo-valgrind 328 | - name: Test with panic backend (debug) 329 | run: LITHIUM_BACKEND=panic cargo valgrind test 330 | - name: Test with Itanium backend (debug) 331 | run: LITHIUM_BACKEND=itanium cargo valgrind test 332 | - name: Test with std thread locals (debug) 333 | run: LITHIUM_THREAD_LOCAL=std cargo valgrind test 334 | - name: Test with panic backend (release) 335 | run: LITHIUM_BACKEND=panic cargo valgrind test --release 336 | - name: Test with Itanium backend (release) 337 | run: LITHIUM_BACKEND=itanium cargo valgrind test --release 338 | - name: Test with std thread locals (release) 339 | run: LITHIUM_THREAD_LOCAL=std cargo valgrind test --release 340 | 341 | test-stable: 342 | timeout-minutes: 3 343 | runs-on: ubuntu-latest 344 | if: success() || failure() 345 | steps: 346 | - name: Checkout 347 | uses: actions/checkout@v4 348 | - name: Install Rust 349 | run: rustup update stable && rustup default stable 350 | - name: Test with panic backend (debug) 351 | run: cargo test 352 | - name: Test with panic backend (release) 353 | run: cargo test --release 354 | 355 | lint: 356 | timeout-minutes: 1 357 | runs-on: ubuntu-latest 358 | strategy: 359 | fail-fast: false 360 | matrix: 361 | backend: [panic, itanium, seh, emscripten] 362 | thread_local: [std, attribute] 363 | env: 364 | LITHIUM_BACKEND: ${{ matrix.backend }} 365 | LITHIUM_THREAD_LOCAL: ${{ matrix.thread_local }} 366 | steps: 367 | - name: Checkout 368 | uses: actions/checkout@v4 369 | - name: Install Rust 370 | run: rustup update nightly && rustup default nightly 371 | - name: Install rustfmt and clippy 372 | run: rustup component add rustfmt clippy 373 | - name: Rustfmt 374 | run: cargo fmt -- --check 375 | - name: Clippy 376 | run: cargo clippy -- -D warnings 377 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | saved-jobs/ 3 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.9" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" 25 | 26 | [[package]] 27 | name = "anyhow" 28 | version = "1.0.91" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" 31 | 32 | [[package]] 33 | name = "autocfg" 34 | version = "1.4.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 37 | 38 | [[package]] 39 | name = "cast" 40 | version = "0.3.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 49 | 50 | [[package]] 51 | name = "ciborium" 52 | version = "0.2.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 55 | dependencies = [ 56 | "ciborium-io", 57 | "ciborium-ll", 58 | "serde", 59 | ] 60 | 61 | [[package]] 62 | name = "ciborium-io" 63 | version = "0.2.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 66 | 67 | [[package]] 68 | name = "ciborium-ll" 69 | version = "0.2.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 72 | dependencies = [ 73 | "ciborium-io", 74 | "half", 75 | ] 76 | 77 | [[package]] 78 | name = "clap" 79 | version = "4.5.20" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 82 | dependencies = [ 83 | "clap_builder", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_builder" 88 | version = "4.5.20" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 91 | dependencies = [ 92 | "anstyle", 93 | "clap_lex", 94 | ] 95 | 96 | [[package]] 97 | name = "clap_lex" 98 | version = "0.7.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 101 | 102 | [[package]] 103 | name = "criterion" 104 | version = "0.5.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 107 | dependencies = [ 108 | "anes", 109 | "cast", 110 | "ciborium", 111 | "clap", 112 | "criterion-plot", 113 | "is-terminal", 114 | "itertools", 115 | "num-traits", 116 | "once_cell", 117 | "oorandom", 118 | "regex", 119 | "serde", 120 | "serde_derive", 121 | "serde_json", 122 | "tinytemplate", 123 | "walkdir", 124 | ] 125 | 126 | [[package]] 127 | name = "criterion-plot" 128 | version = "0.5.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 131 | dependencies = [ 132 | "cast", 133 | "itertools", 134 | ] 135 | 136 | [[package]] 137 | name = "crunchy" 138 | version = "0.2.2" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 141 | 142 | [[package]] 143 | name = "either" 144 | version = "1.13.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 147 | 148 | [[package]] 149 | name = "half" 150 | version = "2.4.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 153 | dependencies = [ 154 | "cfg-if", 155 | "crunchy", 156 | ] 157 | 158 | [[package]] 159 | name = "hermit-abi" 160 | version = "0.4.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 163 | 164 | [[package]] 165 | name = "is-terminal" 166 | version = "0.4.13" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 169 | dependencies = [ 170 | "hermit-abi", 171 | "libc", 172 | "windows-sys 0.52.0", 173 | ] 174 | 175 | [[package]] 176 | name = "itertools" 177 | version = "0.10.5" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 180 | dependencies = [ 181 | "either", 182 | ] 183 | 184 | [[package]] 185 | name = "itoa" 186 | version = "1.0.11" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 189 | 190 | [[package]] 191 | name = "libc" 192 | version = "0.2.161" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 195 | 196 | [[package]] 197 | name = "lithium" 198 | version = "1.0.3" 199 | dependencies = [ 200 | "anyhow", 201 | "autocfg", 202 | "criterion", 203 | "replace_with", 204 | "rustc_version", 205 | "typeid", 206 | ] 207 | 208 | [[package]] 209 | name = "memchr" 210 | version = "2.7.4" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 213 | 214 | [[package]] 215 | name = "num-traits" 216 | version = "0.2.19" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 219 | dependencies = [ 220 | "autocfg", 221 | ] 222 | 223 | [[package]] 224 | name = "once_cell" 225 | version = "1.20.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 228 | 229 | [[package]] 230 | name = "oorandom" 231 | version = "11.1.4" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" 234 | 235 | [[package]] 236 | name = "proc-macro2" 237 | version = "1.0.89" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 240 | dependencies = [ 241 | "unicode-ident", 242 | ] 243 | 244 | [[package]] 245 | name = "quote" 246 | version = "1.0.37" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 249 | dependencies = [ 250 | "proc-macro2", 251 | ] 252 | 253 | [[package]] 254 | name = "regex" 255 | version = "1.11.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 258 | dependencies = [ 259 | "aho-corasick", 260 | "memchr", 261 | "regex-automata", 262 | "regex-syntax", 263 | ] 264 | 265 | [[package]] 266 | name = "regex-automata" 267 | version = "0.4.8" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 270 | dependencies = [ 271 | "aho-corasick", 272 | "memchr", 273 | "regex-syntax", 274 | ] 275 | 276 | [[package]] 277 | name = "regex-syntax" 278 | version = "0.8.5" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 281 | 282 | [[package]] 283 | name = "replace_with" 284 | version = "0.1.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" 287 | 288 | [[package]] 289 | name = "rustc_version" 290 | version = "0.4.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 293 | dependencies = [ 294 | "semver", 295 | ] 296 | 297 | [[package]] 298 | name = "ryu" 299 | version = "1.0.18" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 302 | 303 | [[package]] 304 | name = "same-file" 305 | version = "1.0.6" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 308 | dependencies = [ 309 | "winapi-util", 310 | ] 311 | 312 | [[package]] 313 | name = "semver" 314 | version = "1.0.23" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 317 | 318 | [[package]] 319 | name = "serde" 320 | version = "1.0.213" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" 323 | dependencies = [ 324 | "serde_derive", 325 | ] 326 | 327 | [[package]] 328 | name = "serde_derive" 329 | version = "1.0.213" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" 332 | dependencies = [ 333 | "proc-macro2", 334 | "quote", 335 | "syn", 336 | ] 337 | 338 | [[package]] 339 | name = "serde_json" 340 | version = "1.0.132" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 343 | dependencies = [ 344 | "itoa", 345 | "memchr", 346 | "ryu", 347 | "serde", 348 | ] 349 | 350 | [[package]] 351 | name = "syn" 352 | version = "2.0.85" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 355 | dependencies = [ 356 | "proc-macro2", 357 | "quote", 358 | "unicode-ident", 359 | ] 360 | 361 | [[package]] 362 | name = "tinytemplate" 363 | version = "1.2.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 366 | dependencies = [ 367 | "serde", 368 | "serde_json", 369 | ] 370 | 371 | [[package]] 372 | name = "typeid" 373 | version = "1.0.2" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" 376 | 377 | [[package]] 378 | name = "unicode-ident" 379 | version = "1.0.13" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 382 | 383 | [[package]] 384 | name = "walkdir" 385 | version = "2.5.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 388 | dependencies = [ 389 | "same-file", 390 | "winapi-util", 391 | ] 392 | 393 | [[package]] 394 | name = "winapi-util" 395 | version = "0.1.9" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 398 | dependencies = [ 399 | "windows-sys 0.59.0", 400 | ] 401 | 402 | [[package]] 403 | name = "windows-sys" 404 | version = "0.52.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 407 | dependencies = [ 408 | "windows-targets", 409 | ] 410 | 411 | [[package]] 412 | name = "windows-sys" 413 | version = "0.59.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 416 | dependencies = [ 417 | "windows-targets", 418 | ] 419 | 420 | [[package]] 421 | name = "windows-targets" 422 | version = "0.52.6" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 425 | dependencies = [ 426 | "windows_aarch64_gnullvm", 427 | "windows_aarch64_msvc", 428 | "windows_i686_gnu", 429 | "windows_i686_gnullvm", 430 | "windows_i686_msvc", 431 | "windows_x86_64_gnu", 432 | "windows_x86_64_gnullvm", 433 | "windows_x86_64_msvc", 434 | ] 435 | 436 | [[package]] 437 | name = "windows_aarch64_gnullvm" 438 | version = "0.52.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 441 | 442 | [[package]] 443 | name = "windows_aarch64_msvc" 444 | version = "0.52.6" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 447 | 448 | [[package]] 449 | name = "windows_i686_gnu" 450 | version = "0.52.6" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 453 | 454 | [[package]] 455 | name = "windows_i686_gnullvm" 456 | version = "0.52.6" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 459 | 460 | [[package]] 461 | name = "windows_i686_msvc" 462 | version = "0.52.6" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 465 | 466 | [[package]] 467 | name = "windows_x86_64_gnu" 468 | version = "0.52.6" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 471 | 472 | [[package]] 473 | name = "windows_x86_64_gnullvm" 474 | version = "0.52.6" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 477 | 478 | [[package]] 479 | name = "windows_x86_64_msvc" 480 | version = "0.52.6" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 483 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lithium" 3 | description = "Lightweight exceptions" 4 | license = "Apache-2.0 OR MIT" 5 | repository = "https://github.com/iex-rs/lithium" 6 | readme = "README.md" 7 | keywords = ["error", "error-handling", "exception"] 8 | categories = ["rust-patterns", "no-std"] 9 | version = "1.0.3" 10 | edition = "2021" 11 | links = "rustlithium" # Force uniqueness of crate version 12 | 13 | [dependencies] 14 | typeid = "1.0.2" 15 | 16 | [dev-dependencies] 17 | anyhow = "1" 18 | criterion = { version = "0.5", default-features = false, features = ["cargo_bench_support"] } 19 | replace_with = "0.1.7" 20 | 21 | [build-dependencies] 22 | autocfg = "1.4.0" 23 | rustc_version = "0.4.1" 24 | 25 | [features] 26 | sound-under-stacked-borrows = [] 27 | 28 | [package.metadata."docs.rs"] 29 | all-features = true 30 | 31 | [[bench]] 32 | name = "bench" 33 | harness = false 34 | 35 | [lints.rust.unexpected_cfgs] 36 | level = "warn" 37 | check-cfg = [ 38 | "cfg(abort, values(\"std\", \"core\"))", 39 | "cfg(backend, values(\"itanium\", \"seh\", \"emscripten\", \"panic\", \"unimplemented\"))", 40 | "cfg(thread_local, values(\"std\", \"attribute\", \"unimplemented\"))", 41 | ] 42 | 43 | [profile.dev] 44 | panic = "unwind" 45 | 46 | [profile.release] 47 | panic = "unwind" 48 | 49 | # Use newer versions of emscripten and Node than cross ships by default 50 | [workspace.metadata.cross.target.wasm32-unknown-emscripten] 51 | image = "ghcr.io/iex-rs/wasm32-unknown-emscripten:latest" 52 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = ["LITHIUM_BACKEND", "LITHIUM_THREAD_LOCAL"] 3 | 4 | [target.mips-unknown-linux-gnu] 5 | build-std = ["std"] 6 | 7 | [target.mips64-unknown-linux-gnuabi64] 8 | build-std = ["std"] 9 | 10 | [target.mips64el-unknown-linux-gnuabi64] 11 | build-std = ["std"] 12 | 13 | [target.mipsel-unknown-linux-gnu] 14 | build-std = ["std"] 15 | -------------------------------------------------------------------------------- /LICENSE-APACHE-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Alisa Sireneva 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lithium 2 | 3 | ![License](https://img.shields.io/crates/l/lithium) [![Version](https://img.shields.io/crates/v/lithium)](https://crates.io/crates/lithium) [![docs.rs](https://img.shields.io/docsrs/lithium)](https://docs.rs/lithium) ![Tests](https://github.com/iex-rs/lithium/actions/workflows/ci.yml/badge.svg) 4 | 5 | Lightweight exceptions. 6 | 7 | Lithium provides a custom exception mechanism as an alternative to Rust panics. Compared to Rust panics, this mechanism is allocation-free, avoids indirections and RTTI, and is hence faster, if less applicable. 8 | 9 | On nightly, Lithium is more than 2x faster than Rust panics on common `Result`-like usecases. See the [benchmark](benches/bench.rs). 10 | 11 | See [documentation](https://docs.rs/lithium) for usage and installation instructions. 12 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 3 | use lithium::{catch, intercept, throw}; 4 | 5 | fn bench_anyhow(c: &mut Criterion) { 6 | fn rust() { 7 | fn imp(n: u32) { 8 | let n = black_box(n); 9 | if n == 0 { 10 | std::panic::resume_unwind(Box::new(anyhow!("Hello, world!"))); 11 | } else { 12 | match std::panic::catch_unwind(|| imp(n - 1)) { 13 | Ok(x) => x, 14 | Err(mut bx) => { 15 | let err = bx.downcast_mut::().unwrap(); 16 | replace_with::replace_with_or_abort(err, |e| e.context("In imp")); 17 | std::panic::resume_unwind(bx); 18 | } 19 | } 20 | } 21 | } 22 | let _ = black_box(std::panic::catch_unwind(|| { 23 | imp(5); 24 | })); 25 | } 26 | 27 | fn lithium() { 28 | fn imp(n: u32) { 29 | let n = black_box(n); 30 | unsafe { 31 | if n == 0 { 32 | throw(anyhow!("Hello, world!")); 33 | } else { 34 | match intercept::<(), anyhow::Error>(|| imp(n - 1)) { 35 | Ok(x) => x, 36 | Err((e, in_flight)) => in_flight.rethrow(e.context("In imp")), 37 | } 38 | } 39 | } 40 | } 41 | let _ = black_box(unsafe { 42 | catch::<(), anyhow::Error>(|| { 43 | imp(5); 44 | }) 45 | }); 46 | } 47 | 48 | let mut group = c.benchmark_group("anyhow"); 49 | group.bench_function("Rust", |b| b.iter(|| rust())); 50 | group.bench_function("Lithium", |b| b.iter(|| lithium())); 51 | group.finish(); 52 | } 53 | 54 | fn bench_simple(c: &mut Criterion) { 55 | fn rust() { 56 | fn imp(n: u32) { 57 | let n = black_box(n); 58 | if n == 0 { 59 | std::panic::resume_unwind(Box::new("Hello, world!")); 60 | } else { 61 | match std::panic::catch_unwind(|| imp(n - 1)) { 62 | Ok(x) => x, 63 | Err(mut bx) => { 64 | let err = bx.downcast_mut::<&'static str>().unwrap(); 65 | *err = black_box(*err); // simulate adding information to the error in some fashion 66 | std::panic::resume_unwind(bx); 67 | } 68 | } 69 | } 70 | } 71 | let _ = black_box(std::panic::catch_unwind(|| { 72 | imp(5); 73 | })); 74 | } 75 | 76 | fn lithium() { 77 | fn imp(n: u32) { 78 | let n = black_box(n); 79 | unsafe { 80 | if n == 0 { 81 | throw("Hello, world!"); 82 | } else { 83 | match intercept::<(), &'static str>(|| imp(n - 1)) { 84 | Ok(x) => x, 85 | Err((e, in_flight)) => in_flight.rethrow(black_box(e)), // simulate adding information 86 | } 87 | } 88 | } 89 | } 90 | let _ = black_box(unsafe { 91 | catch::<(), &'static str>(|| { 92 | imp(5); 93 | }) 94 | }); 95 | } 96 | 97 | let mut group = c.benchmark_group("simple"); 98 | group.bench_function("Rust", |b| b.iter(|| rust())); 99 | group.bench_function("Lithium", |b| b.iter(|| lithium())); 100 | group.finish(); 101 | } 102 | 103 | criterion_group!(benches, bench_anyhow, bench_simple); 104 | criterion_main!(benches); 105 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use rustc_version::{version_meta, Channel}; 2 | 3 | fn has_cfg(name: &str) -> bool { 4 | std::env::var_os(format!("CARGO_CFG_{}", name.to_uppercase())).is_some() 5 | } 6 | 7 | fn cfg(name: &str) -> String { 8 | std::env::var(format!("CARGO_CFG_{}", name.to_uppercase())).unwrap_or_default() 9 | } 10 | 11 | fn main() { 12 | println!("cargo::rerun-if-env-changed=MIRIFLAGS"); 13 | let is_miri = has_cfg("miri"); 14 | let is_tree_borrows = 15 | std::env::var("MIRIFLAGS").is_ok_and(|flags| flags.contains("-Zmiri-tree-borrows")); 16 | if is_miri && !is_tree_borrows { 17 | println!("cargo::rustc-cfg=feature=\"sound-under-stacked-borrows\""); 18 | } 19 | 20 | let ac = autocfg::new(); 21 | let is_nightly = version_meta().unwrap().channel == Channel::Nightly; 22 | 23 | println!("cargo::rerun-if-env-changed=LITHIUM_THREAD_LOCAL"); 24 | if let Ok(thread_local) = std::env::var("LITHIUM_THREAD_LOCAL") { 25 | println!("cargo::rustc-cfg=thread_local=\"{thread_local}\""); 26 | } else if is_nightly && has_cfg("target_thread_local") { 27 | println!("cargo::rustc-cfg=thread_local=\"attribute\""); 28 | } else if ac 29 | .probe_raw( 30 | r" 31 | #![no_std] 32 | extern crate std; 33 | std::thread_local! { 34 | static FOO: () = (); 35 | } 36 | ", 37 | ) 38 | .is_ok() 39 | { 40 | println!("cargo::rustc-cfg=thread_local=\"std\""); 41 | } else { 42 | println!("cargo::rustc-cfg=thread_local=\"unimplemented\""); 43 | } 44 | 45 | println!("cargo::rerun-if-env-changed=LITHIUM_BACKEND"); 46 | if let Ok(backend) = std::env::var("LITHIUM_BACKEND") { 47 | println!("cargo::rustc-cfg=backend=\"{backend}\""); 48 | } else if is_nightly && cfg("target_os") == "emscripten" && !has_cfg("emscripten_wasm_eh") { 49 | println!("cargo::rustc-cfg=backend=\"emscripten\""); 50 | } else if is_nightly 51 | && (has_cfg("unix") 52 | || (has_cfg("windows") && cfg("target_env") == "gnu") 53 | || cfg("target_arch") == "wasm32") 54 | { 55 | println!("cargo::rustc-cfg=backend=\"itanium\""); 56 | } else if is_nightly && (has_cfg("windows") && cfg("target_env") == "msvc") && !is_miri { 57 | println!("cargo::rustc-cfg=backend=\"seh\""); 58 | } else if ac 59 | .probe_raw( 60 | r" 61 | #![no_std] 62 | extern crate std; 63 | use std::panic::{catch_unwind, resume_unwind}; 64 | ", 65 | ) 66 | .is_ok() 67 | { 68 | println!("cargo::rustc-cfg=backend=\"panic\""); 69 | } else { 70 | println!("cargo::rustc-cfg=backend=\"unimplemented\""); 71 | } 72 | 73 | if ac 74 | .probe_raw( 75 | r#" 76 | #![no_std] 77 | extern crate std; 78 | use std::io::Write; 79 | fn main() { 80 | let _ = std::io::stderr().write_all(b"hello"); 81 | std::process::abort(); 82 | } 83 | "#, 84 | ) 85 | .is_ok() 86 | { 87 | println!("cargo::rustc-cfg=abort=\"std\""); 88 | } else { 89 | println!("cargo::rustc-cfg=abort=\"core\""); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ci/cargo-save: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if [[ "$1" == --run ]]; then 5 | mkdir -p saved-jobs 6 | 7 | description="$2" 8 | path="$(mktemp -p saved-jobs)" 9 | cp "$3" "$path" 10 | chmod +x "$path" 11 | shift 3 12 | 13 | printf -v code "%q " run "$description" "$path" "$@" 14 | echo "$code" >>saved-jobs/list 15 | exec "$path" "$@" 16 | fi 17 | 18 | description="$1" 19 | shift 20 | # Explicitly specifying 'bash' is necessary for Windows 21 | exec cargo --config "target.'$target'.runner = ['bash', '$(dirname "$0")/cargo-save', '--run', '$description']" "$@" 22 | -------------------------------------------------------------------------------- /ci/cargo-wasi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export RUSTFLAGS="$RUSTFLAGS -C panic=unwind" 3 | exec cargo \ 4 | --config "target.'wasm32-wasip1'.runner = ['node', '--no-warnings', '$(dirname "$0")/wasi.mjs', 'preview1']" \ 5 | --config "target.'wasm32-wasip2'.runner = ['node', '--no-warnings', '$(dirname "$0")/wasi.mjs', 'preview2']" \ 6 | -Z build-std=core,std \ 7 | "$@" 8 | -------------------------------------------------------------------------------- /ci/run-saved-jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | run() { 5 | echo "::group::$1" 6 | shift 7 | wine "$@" 8 | echo ::endgroup:: 9 | } 10 | 11 | . saved-jobs/list 12 | -------------------------------------------------------------------------------- /ci/wasi.mjs: -------------------------------------------------------------------------------- 1 | import { WASI } from "node:wasi"; 2 | import { readFile } from "node:fs/promises"; 3 | 4 | const wasi = new WASI({ 5 | version: process.argv[2], 6 | args: process.argv.slice(3), 7 | env: process.env, 8 | preopens: { 9 | "/": "." 10 | }, 11 | }); 12 | 13 | const wasm = await WebAssembly.compile(await readFile(process.argv[3])); 14 | const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject()); 15 | 16 | wasi.start(instance); 17 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | check-private-items = true 2 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use super::backend::{ActiveBackend, RethrowHandle, ThrowByValue}; 2 | 3 | // Module invariant: thrown exceptions of type `E` are passed to the backend as instance of 4 | // `Exception` with the cause filled, which is immediately read out upon catch. 5 | 6 | /// Throw an exception. 7 | /// 8 | /// # Safety 9 | /// 10 | /// See the safety section of [this crate](crate) for information on matching types. 11 | /// 12 | /// In addition, the caller must ensure that the exception can only be caught by Lithium functions 13 | /// and not by the system runtime. The list of banned functions includes 14 | /// [`std::panic::catch_unwind`] and [`std::thread::spawn`], as well as throwing from `main`. 15 | /// 16 | /// For this reason, the caller must ensure no frames between [`throw`] and [`catch`] can catch the 17 | /// exception. This includes not passing throwing callbacks to foreign crates, but also not using 18 | /// [`throw`] in own code that might [`intercept`] an exception without cooperation with the 19 | /// throwing side. 20 | /// 21 | /// # Example 22 | /// 23 | /// ```should_panic 24 | /// use lithium::throw; 25 | /// 26 | /// unsafe { 27 | /// throw::<&'static str>("Oops!"); 28 | /// } 29 | /// ``` 30 | #[inline(never)] 31 | pub unsafe fn throw(cause: E) -> ! { 32 | // SAFETY: Required transitively. 33 | unsafe { 34 | ActiveBackend::throw(cause); 35 | } 36 | } 37 | 38 | /// Catch an exception. 39 | /// 40 | /// If `func` returns a value, this function wraps it in [`Ok`]. 41 | /// 42 | /// If `func` throws an exception, this function returns it, wrapped it in [`Err`]. 43 | /// 44 | /// If you need to rethrow the exception, possibly modifying it in the process, consider using the 45 | /// more efficient [`intercept`] function instead of pairing [`catch`] with [`throw`]. 46 | /// 47 | /// Rust panics are propagated as-is and not caught. 48 | /// 49 | /// # Safety 50 | /// 51 | /// `func` must only throw exceptions of type `E`. See the safety section of [this crate](crate) for 52 | /// more information. 53 | /// 54 | /// # Example 55 | /// 56 | /// ```rust 57 | /// use lithium::{catch, throw}; 58 | /// 59 | /// // SAFETY: the exception type matches 60 | /// let res = unsafe { 61 | /// catch::<(), &'static str>(|| throw::<&'static str>("Oops!")) 62 | /// }; 63 | /// 64 | /// assert_eq!(res, Err("Oops!")); 65 | /// ``` 66 | #[expect( 67 | clippy::missing_errors_doc, 68 | reason = "`Err` value is described immediately" 69 | )] 70 | #[inline] 71 | pub unsafe fn catch(func: impl FnOnce() -> R) -> Result { 72 | // SAFETY: 73 | // - `func` only throws `E` by the safety requirement. 74 | // - `InFlightException` is immediately dropped before returning from `catch`, so no exceptions 75 | // may be thrown while it's alive. 76 | unsafe { intercept(func) }.map_err(|(cause, _)| cause) 77 | } 78 | 79 | /// Not-quite-caught exception. 80 | /// 81 | /// This type is returned by [`intercept`] when an exception is caught. Exception handling is not 82 | /// yet done at that point: it's akin to entering a `catch` clause in C++. 83 | /// 84 | /// At this point, you can either drop the handle, which halts the Lithium machinery and brings you 85 | /// back to the sane land of [`Result`], or call [`InFlightException::rethrow`] to piggy-back on the 86 | /// contexts of the caught exception. 87 | pub struct InFlightException(::RethrowHandle); 88 | 89 | impl InFlightException { 90 | /// Throw a new exception by reusing the existing context. 91 | /// 92 | /// See [`intercept`] docs for examples and safety notes. 93 | /// 94 | /// # Safety 95 | /// 96 | /// See the safety section of [this crate](crate) for information on matching types. 97 | /// 98 | /// In addition, the caller must ensure that the exception can only be caught by Lithium 99 | /// functions and not by the system runtime. The list of banned functions includes 100 | /// [`std::panic::catch_unwind`] and [`std::thread::spawn`]. 101 | /// 102 | /// For this reason, the caller must ensure no frames between `rethrow` and [`catch`] can catch 103 | /// the exception. This includes not passing throwing callbacks to foreign crates, but also not 104 | /// using `rethrow` in own code that might [`intercept`] an exception without cooperation with 105 | /// the throwing side. 106 | #[inline] 107 | pub unsafe fn rethrow(self, new_cause: F) -> ! { 108 | // SAFETY: Requirements forwarded. 109 | unsafe { 110 | self.0.rethrow(new_cause); 111 | } 112 | } 113 | } 114 | 115 | /// Begin exception catching. 116 | /// 117 | /// If `func` returns a value, this function wraps it in [`Ok`]. 118 | /// 119 | /// If `func` throws an exception, the error cause along with a handle to the exception is returned 120 | /// in [`Err`]. This handle can be used to rethrow the exception, possibly modifying its value or 121 | /// type in the process. 122 | /// 123 | /// If you always need to catch the exception, use [`catch`] instead. This function is mostly useful 124 | /// as an analogue of [`Result::map_err`]. 125 | /// 126 | /// Rust panics are propagated as-is and not caught. 127 | /// 128 | /// # Safety 129 | /// 130 | /// `func` must only throw exceptions of type `E`. See the safety section of [this crate](crate) for 131 | /// more information. 132 | /// 133 | /// **In addition**, certain requirements are imposed on how the returned [`InFlightException`] is 134 | /// used. In particular, no exceptions may be thrown between the moment this function returns 135 | /// an [`InFlightException`] and the moment it is dropped (either by calling [`drop`] or by calling 136 | /// its [`InFlightException::rethrow`] method). Panics, however, are allowed. 137 | /// 138 | /// Caught exceptions are not subject to this requirement, i.e. the following pattern is safe: 139 | /// 140 | /// ```rust 141 | /// use lithium::{intercept, throw}; 142 | /// 143 | /// unsafe { 144 | /// let result = intercept::<(), i32>(|| throw::(1)); 145 | /// drop(intercept::<(), i32>(|| throw::(2))); 146 | /// drop(result); 147 | /// } 148 | /// ``` 149 | /// 150 | /// # Example 151 | /// 152 | /// ```rust 153 | /// use anyhow::{anyhow, Error, Context}; 154 | /// use lithium::{catch, intercept, throw}; 155 | /// 156 | /// /// Throws [`Error`]. 157 | /// unsafe fn f() { 158 | /// throw::(anyhow!("f failed")); 159 | /// } 160 | /// 161 | /// /// Throws [`Error`]. 162 | /// unsafe fn g() { 163 | /// // SAFETY: 164 | /// // - f only ever throws Error 165 | /// // - no exception is thrown between `intercept` returning and call to `rethrow` 166 | /// match intercept::<_, Error>(|| f()) { 167 | /// Ok(x) => x, 168 | /// Err((e, handle)) => handle.rethrow(e.context("in g")), 169 | /// } 170 | /// } 171 | /// 172 | /// // SAFETY: g only ever throws Error 173 | /// println!("{}", unsafe { catch::<_, Error>(|| g()) }.unwrap_err()); 174 | /// ``` 175 | #[expect( 176 | clippy::missing_errors_doc, 177 | reason = "`Err` value is described immediately" 178 | )] 179 | #[inline(always)] 180 | pub unsafe fn intercept(func: impl FnOnce() -> R) -> Result)> { 181 | // SAFETY: Requirements forwarded. 182 | unsafe { ActiveBackend::intercept(func) } 183 | .map_err(|(cause, handle)| (cause, InFlightException(handle))) 184 | } 185 | 186 | #[cfg(test)] 187 | mod test { 188 | use super::*; 189 | use alloc::string::String; 190 | 191 | #[test] 192 | fn catch_ok() { 193 | let result: Result = unsafe { catch(|| String::from("Hello, world!")) }; 194 | assert_eq!(result.unwrap(), "Hello, world!"); 195 | } 196 | 197 | #[test] 198 | fn catch_err() { 199 | let result: Result<(), String> = unsafe { catch(|| throw(String::from("Hello, world!"))) }; 200 | assert_eq!(result.unwrap_err(), "Hello, world!"); 201 | } 202 | 203 | #[test] 204 | fn catch_panic() { 205 | struct Dropper<'a>(&'a mut bool); 206 | impl Drop for Dropper<'_> { 207 | fn drop(&mut self) { 208 | *self.0 = true; 209 | } 210 | } 211 | 212 | let mut destructor_was_run = false; 213 | std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 214 | let _dropper = Dropper(&mut destructor_was_run); 215 | let _: Result<(), ()> = unsafe { catch(|| panic!("Hello, world!")) }; 216 | })) 217 | .unwrap_err(); 218 | assert!(destructor_was_run); 219 | 220 | // Ensure that panic count is reset to 0 221 | assert!(!std::thread::panicking()); 222 | } 223 | 224 | #[test] 225 | fn rethrow() { 226 | let result: Result<(), String> = unsafe { 227 | catch(|| { 228 | let (err, in_flight): (String, _) = 229 | intercept(|| throw(String::from("Hello, world!"))).unwrap_err(); 230 | in_flight.rethrow(err + " You look nice btw."); 231 | }) 232 | }; 233 | assert_eq!(result.unwrap_err(), "Hello, world! You look nice btw."); 234 | } 235 | 236 | #[test] 237 | fn panic_while_in_flight() { 238 | struct Dropper; 239 | impl Drop for Dropper { 240 | fn drop(&mut self) { 241 | let _ = std::panic::catch_unwind(|| { 242 | let (_err, _in_flight): (String, _) = unsafe { 243 | intercept(|| throw(String::from("Literally so insanely suspicious"))) 244 | } 245 | .unwrap_err(); 246 | panic!("Would be a shame if something happened to the exception."); 247 | }); 248 | } 249 | } 250 | 251 | let result: Result<(), String> = unsafe { 252 | catch(|| { 253 | let _dropper = Dropper; 254 | throw(String::from("Hello, world!")); 255 | }) 256 | }; 257 | assert_eq!(result.unwrap_err(), "Hello, world!"); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/backend/emscripten.rs: -------------------------------------------------------------------------------- 1 | // This is partially taken from 2 | // - https://github.com/rust-lang/rust/blob/master/library/panic_unwind/src/emcc.rs 3 | 4 | use super::{ 5 | super::{abort, intrinsic::intercept}, 6 | ThrowByPointer, 7 | }; 8 | 9 | pub(crate) struct ActiveBackend; 10 | 11 | /// Emscripten unwinding. 12 | /// 13 | /// At the moment, the emscripten target doesn't provide a Itanium-compatible ABI, but it does 14 | /// libcxxabi-style C++ exceptions. This is what we're going to use. 15 | // SAFETY: C++ exceptions satisfy the requirements. 16 | unsafe impl ThrowByPointer for ActiveBackend { 17 | type ExceptionHeader = Header; 18 | 19 | fn new_header() -> Header { 20 | Header { 21 | reference_count: 0, 22 | exception_type: core::ptr::null(), 23 | exception_destructor: None, 24 | caught: false, 25 | rethrown: false, 26 | adjusted_ptr: core::ptr::null_mut(), 27 | padding: core::ptr::null(), 28 | } 29 | } 30 | 31 | #[inline] 32 | unsafe fn throw(ex: *mut Header) -> ! { 33 | // SAFETY: This is in-bounds for the header. 34 | let end_of_header = unsafe { ex.add(1) }.cast(); 35 | 36 | // SAFETY: We provide a valid exception header. 37 | unsafe { 38 | __cxa_throw(end_of_header, &raw const TYPE_INFO, cleanup); 39 | } 40 | } 41 | 42 | #[inline(always)] 43 | fn intercept R, R>(func: Func) -> Result { 44 | let ptr = match intercept(func, |ex| { 45 | // SAFETY: `core::intrinsics::catch_unwind` provides a pointer to a stack-allocated 46 | // instance of `CatchData`. It needs to be read inside the `intercept` callback because 47 | // it'll be dead by the moment `intercept` returns. 48 | #[expect( 49 | clippy::cast_ptr_alignment, 50 | reason = "guaranteed to be aligned by rustc" 51 | )] 52 | unsafe { 53 | (*ex.cast::()).ptr 54 | } 55 | }) { 56 | Ok(result) => return Ok(result), 57 | Err(ptr) => ptr, 58 | }; 59 | 60 | // SAFETY: `ptr` was obtained from a `core::intrinsics::catch_unwind` call. 61 | let adjusted_ptr = unsafe { __cxa_begin_catch(ptr) }; 62 | 63 | // SAFETY: `adjusted_ptr` points at what the unwinder thinks is a beginning of our exception 64 | // object. In reality, this is just the ned of header, so `sub(1)` yields the beginning of 65 | // the header. 66 | let ex: *mut Header = unsafe { adjusted_ptr.cast::
().sub(1) }; 67 | 68 | // SAFETY: `ex` points at a valid header. We're unique, so no data races are possible. 69 | if unsafe { (*ex).exception_type } != &raw const TYPE_INFO { 70 | // Rust panic or a foreign exception. Either way, rethrow. 71 | // SAFETY: This function has no preconditions. 72 | unsafe { 73 | __cxa_rethrow(); 74 | } 75 | } 76 | 77 | // Prevent `__cxa_end_catch` from trying to deallocate the exception object with free(3) and 78 | // corrupting the heap. 79 | // SAFETY: We require that Lithium exceptions are not caught by foreign runtimes, so we 80 | // assume this is still a unique reference to this exception. 81 | unsafe { 82 | (*ex).reference_count = 2; 83 | } 84 | 85 | // SAFETY: This function has no preconditions. 86 | unsafe { 87 | __cxa_end_catch(); 88 | } 89 | 90 | Err(ex) 91 | } 92 | } 93 | 94 | // This is __cxa_exception from emscripten sources. 95 | #[repr(C)] 96 | pub(crate) struct Header { 97 | reference_count: usize, 98 | exception_type: *const TypeInfo, 99 | exception_destructor: Option *mut ()>, 100 | caught: bool, 101 | rethrown: bool, 102 | adjusted_ptr: *mut (), 103 | padding: *const (), 104 | } 105 | 106 | // This is std::type_info. 107 | #[repr(C)] 108 | struct TypeInfo { 109 | vtable: *const usize, 110 | name: *const i8, 111 | } 112 | 113 | // SAFETY: `!Sync` pointers are stupid. 114 | unsafe impl Sync for TypeInfo {} 115 | 116 | #[repr(C)] 117 | struct CatchData { 118 | ptr: *mut (), 119 | is_rust_panic: bool, 120 | } 121 | 122 | extern "C" { 123 | #[link_name = "\x01_ZTVN10__cxxabiv117__class_type_infoE"] 124 | static CLASS_TYPE_INFO_VTABLE: [usize; 3]; 125 | } 126 | 127 | static TYPE_INFO: TypeInfo = TypeInfo { 128 | // Normally we would use .as_ptr().add(2) but this doesn't work in a const context. 129 | vtable: unsafe { &CLASS_TYPE_INFO_VTABLE[2] }, 130 | name: c"lithium_exception".as_ptr(), 131 | }; 132 | 133 | extern "C-unwind" { 134 | fn __cxa_begin_catch(thrown_exception: *mut ()) -> *mut (); 135 | 136 | fn __cxa_rethrow() -> !; 137 | 138 | fn __cxa_end_catch(); 139 | 140 | fn __cxa_throw( 141 | thrown_object: *mut (), 142 | tinfo: *const TypeInfo, 143 | destructor: unsafe extern "C" fn(*mut ()) -> *mut (), 144 | ) -> !; 145 | } 146 | 147 | /// Destruct an exception when caught by a foreign runtime. 148 | /// 149 | /// # Safety 150 | /// 151 | /// `ex` must point at a valid exception object. 152 | unsafe extern "C" fn cleanup(_ex: *mut ()) -> *mut () { 153 | abort("A Lithium exception was caught by a non-Lithium catch mechanism. This is undefined behavior. The process will now terminate.\n"); 154 | } 155 | -------------------------------------------------------------------------------- /src/backend/itanium.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | super::{abort, intrinsic::intercept}, 3 | ThrowByPointer, 4 | }; 5 | use core::mem::MaybeUninit; 6 | 7 | pub const LITHIUM_EXCEPTION_CLASS: u64 = u64::from_ne_bytes(*b"RUSTIEX\0"); 8 | 9 | pub(crate) struct ActiveBackend; 10 | 11 | // SAFETY: We use Itanium EH ABI, which supports nested exceptions correctly. We can assume we don't 12 | // encounter foreign frames, because that's a safety requirement of `throw`. 13 | unsafe impl ThrowByPointer for ActiveBackend { 14 | type ExceptionHeader = Header; 15 | 16 | fn new_header() -> Header { 17 | Header { 18 | class: LITHIUM_EXCEPTION_CLASS, 19 | cleanup: Some(cleanup), 20 | // ARM EH ABI [1] requires that the first private field is initialized to 0 before the 21 | // unwind routines see it. This is not necessary for other architectures (except C6x), 22 | // but being consistent doesn't hurt. In practice, libgcc uses this field to store force 23 | // unwinding information, so leaving this uninitialized leads to SIGILLs and SIGSEGVs 24 | // because it uses the field as a callback address. Strictly speaking, we should 25 | // reinitialize this field back to zero when we do `_Unwind_RaiseException` later, but 26 | // this is unnecessary for libgcc, and libunwind uses the cross-platform mechanism for 27 | // ARM too. 28 | // [1]: https://github.com/ARM-software/abi-aa/blob/76d56124610302e645b66ac4e491be0c1a90ee11/ehabi32/ehabi32.rst#language-independent-unwinding-types-and-functions 29 | private1: core::ptr::null(), 30 | private_rest: MaybeUninit::uninit(), 31 | } 32 | } 33 | 34 | #[inline] 35 | unsafe fn throw(ex: *mut Header) -> ! { 36 | // SAFETY: We provide a valid exception header. 37 | unsafe { 38 | raise(ex.cast()); 39 | } 40 | } 41 | 42 | #[inline(always)] 43 | fn intercept R, R>(func: Func) -> Result { 44 | let ex = match intercept(func, |ex| ex) { 45 | Ok(value) => return Ok(value), 46 | Err(ex) => ex, 47 | }; 48 | 49 | // SAFETY: `ex` is a pointer to an exception object as provided by the unwinder, so it must 50 | // be valid for reads. It's not explicitly documented that the class is not modified in 51 | // runtime, but that sounds like common sense. Note that we only dereference the class 52 | // rather than the whole `Header`, as we don't know whether `ex` is aligned to `Header`, but 53 | // it must be at least aligned for `u64` access. 54 | #[expect(clippy::cast_ptr_alignment, reason = "See the safety comment above")] 55 | let class = unsafe { *ex.cast::() }; 56 | 57 | if class != LITHIUM_EXCEPTION_CLASS { 58 | // SAFETY: The EH ABI allows rethrowing foreign exceptions under the following 59 | // conditions: 60 | // - The exception is not modified or otherwise interacted with. We don't do this, 61 | // expect for determining whether it's foreign in the first place. 62 | // - Runtime EH-related functions are not invoked between catching the exception and 63 | // rethrowing it. We don't do that. 64 | // - The foreign exception is not active at the same time as another exception. We don't 65 | // trigger exceptions between catch and rethrow, so we only have to rule out the 66 | // foreign exception being nested prior to our catch. This is somewhat complicated: 67 | // - If the foreign exception is actually a Rust panic, we know from stdlib's code 68 | // that the personality function works just fine with rethrowing regardless of 69 | // nesting. This is not a hard proof, but this is highly unlikely to change. 70 | // - If the foreign exception was produced neither by Rust, nor by Lithium, the case 71 | // is similar to how the behavior of `std::panic::catch_unwind` being unwound by 72 | // a foreign exception is undefined; i.e., it's on the user who allows foreign 73 | // exceptions to travel through Lithium frames. 74 | // If project-ffi-unwind changes the rustc behavior, we might have to update this 75 | // code. 76 | unsafe { 77 | raise(ex); 78 | } 79 | } 80 | 81 | Err(ex.cast()) 82 | } 83 | } 84 | 85 | // The alignment on this structure is... complicated. GCC uses `__attribute__((aligned))` here and 86 | // expects everyone else to do the same, but we don't have that in Rust. The rules for computing the 87 | // default (maximum) alignment are unclear. If we guess too low, the unwinder might access unaligned 88 | // data, so we use 16 bytes on all platforms to keep safe. This includes 32-bit machines, becuase on 89 | // i386 `__attribute__((aligned))` aligns to 16 bytes too. Therefore, the alignment of this 90 | // structure might be larger than the actual alignment when we access foreign exceptions, so we 91 | // can't use this type for that. 92 | #[repr(C, align(16))] 93 | pub struct Header { 94 | class: u64, 95 | cleanup: Option, 96 | // See `new_header` for why this needs to be a separate field. 97 | private1: *const (), 98 | private_rest: MaybeUninit<[*const (); get_unwinder_private_word_count() - 1]>, 99 | } 100 | 101 | // Data from https://github.com/rust-lang/rust/blob/master/library/unwind/src/libunwind.rs 102 | const fn get_unwinder_private_word_count() -> usize { 103 | // The Itanium EH ABI says the structure contains 2 private uint64_t words. Some architectures 104 | // decided this means "2 private native words". So on some 32-bit architectures this is two 105 | // 64-bit words, which together with padding amount to 5 native words, and on other 106 | // architectures it's two native words. Others are just morons. 107 | if cfg!(target_arch = "x86") { 108 | 5 109 | } else if cfg!(any( 110 | all(target_arch = "x86_64"), 111 | all(target_arch = "aarch64", target_pointer_width = "64"), 112 | )) { 113 | if cfg!(windows) { 114 | 6 115 | } else { 116 | 2 117 | } 118 | } else if cfg!(target_arch = "arm") { 119 | if cfg!(target_vendor = "apple") { 120 | 5 121 | } else { 122 | 20 123 | } 124 | } else if cfg!(all(target_arch = "aarch64", target_pointer_width = "32")) { 125 | 5 126 | } else if cfg!(target_os = "emscripten") { 127 | 20 128 | } else if cfg!(all(target_arch = "hexagon", target_os = "linux")) { 129 | 35 130 | } else if cfg!(any( 131 | target_arch = "m68k", 132 | target_arch = "mips", 133 | target_arch = "mips32r6", 134 | target_arch = "csky", 135 | target_arch = "mips64", 136 | target_arch = "mips64r6", 137 | target_arch = "powerpc", 138 | target_arch = "powerpc64", 139 | target_arch = "s390x", 140 | target_arch = "sparc", 141 | target_arch = "sparc64", 142 | target_arch = "riscv64", 143 | target_arch = "riscv32", 144 | target_arch = "loongarch64", 145 | target_arch = "wasm32" 146 | )) { 147 | 2 148 | } else { 149 | panic!("Unsupported architecture"); 150 | } 151 | } 152 | 153 | /// Destruct an exception when caught by a foreign runtime. 154 | /// 155 | /// # Safety 156 | /// 157 | /// `ex` must point at a valid exception object. 158 | unsafe extern "C" fn cleanup(_code: i32, _ex: *mut Header) { 159 | abort("A Lithium exception was caught by a non-Lithium catch mechanism. This is undefined behavior. The process will now terminate.\n"); 160 | } 161 | 162 | #[cfg(not(target_arch = "wasm32"))] 163 | extern "C-unwind" { 164 | fn _Unwind_RaiseException(ex: *mut u8) -> !; 165 | } 166 | 167 | /// Raise an Itanium EH ABI-compatible exception. 168 | /// 169 | /// # Safety 170 | /// 171 | /// `ex` must point at a valid instance of `_Unwind_Exception`. 172 | #[inline] 173 | unsafe fn raise(ex: *mut u8) -> ! { 174 | #[cfg(not(target_arch = "wasm32"))] 175 | #[expect(clippy::used_underscore_items, reason = "External API")] 176 | // SAFETY: Passthrough. 177 | unsafe { 178 | _Unwind_RaiseException(ex); 179 | } 180 | 181 | #[cfg(target_arch = "wasm32")] 182 | // SAFETY: Passthrough. 183 | unsafe { 184 | core::arch::wasm32::throw::<0>(ex); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | //! Unwinding backends. 2 | //! 3 | //! Unwinding is a mechanism of forcefully "returning" through multiple call frames, called 4 | //! *throwing*, up until a special call frame, called *interceptor*. This roughly corresponds to the 5 | //! `resume_unwind`/`catch_unwind` pair on Rust and `throw`/`catch` pair on C++. 6 | //! 7 | //! It's crucial that unwinding doesn't require (source-level) cooperation from the intermediate 8 | //! call frames. 9 | //! 10 | //! Two kinds of backends are supported: those that throw by value and those that throw by pointer. 11 | //! Throwing by value is for backends that can keep arbitrary data retained on stack during 12 | //! unwinding (i.e. unwinding and calling landing pads does not override the throwing stackframe), 13 | //! while throwing by pointer is for backends that need the exception, along with any additional 14 | //! data, to be stored on heap. 15 | //! 16 | //! # Safety 17 | //! 18 | //! Backends must ensure that when an exception is thrown, unwinding proceeds to the closest (most 19 | //! nested) `intercept` frame and that `intercept` returns this exact exception. 20 | //! 21 | //! Note that several exceptions can co-exist at once, even in a single thread. This can happen if 22 | //! a destructor that uses exceptions (without letting them escape past `drop`) is invoked during 23 | //! unwinding from another exception. This can be nested arbitrarily. In this context, the order of 24 | //! catching must be in the reverse order of throwing. 25 | //! 26 | //! During unwinding, all destructors of locals must be run, as if `return` was called. Exceptions 27 | //! may not be ignored or caught twice. 28 | 29 | /// Throw-by-pointer backend. 30 | /// 31 | /// Implementors of this trait should consider exceptions as type-erased objects. These objects 32 | /// contain a header, provided by the implementor, and the `throw` and `intercept` method work only 33 | /// with this header. The header is part of a greater allocation containing the exception object, 34 | /// but interacting with this object is forbidden. 35 | /// 36 | /// # Safety 37 | /// 38 | /// Implementations must satisfy the rules of the "Safety" section of [this module](self). In 39 | /// addition: 40 | /// 41 | /// The implementation may modify the header arbitrarily during unwinding, but modifying any other 42 | /// data from the same allocation is forbidden. 43 | /// 44 | /// If the `intercept` method returns `Err`, the returned pointer must be the same as the pointer 45 | /// passed to `throw`, including provenance. 46 | /// 47 | /// The user of this trait is allowed to reuse the header when rethrowing exceptions. In particular, 48 | /// the return value of `intercept` may be used as an argument to `throw`. 49 | #[allow(dead_code, reason = "This is only used by some of the backends")] 50 | pub unsafe trait ThrowByPointer { 51 | /// An exception header. 52 | /// 53 | /// Allocated exceptions, as stored in the [`Exception`](super::exceptions::Exception) type, 54 | /// will contain this header. This allows exception pointers to be used with ABIs that require 55 | /// exceptions to contain custom information, like Itanium EH ABI. 56 | type ExceptionHeader; 57 | 58 | /// Create a new exception header. 59 | /// 60 | /// This will be called whenever a new exception needs to be allocated. 61 | fn new_header() -> Self::ExceptionHeader; 62 | 63 | /// Throw an exception. 64 | /// 65 | /// # Safety 66 | /// 67 | /// The first requirement is that `ex` is a unique pointer to an exception header. 68 | /// 69 | /// Secondly, it is important that intermediate call frames don't preclude unwinding from 70 | /// happening soundly. For example, [`catch_unwind`](std::panic::catch_unwind) can safely catch 71 | /// panics and may start catching foreign exceptions soon, both of which can confuse the user of 72 | /// this trait. 73 | /// 74 | /// For this reason, the caller must ensure no intermediate frames can affect unwinding. This 75 | /// includes not passing throwing callbacks to foreign crates, but also not using `throw` in own 76 | /// code that might `intercept` an exception without cooperation with the throwing side. 77 | unsafe fn throw(ex: *mut Self::ExceptionHeader) -> !; 78 | 79 | /// Catch an exception. 80 | /// 81 | /// This function returns `Ok` if the function returns normally, or `Err` if it throws (and the 82 | /// thrown exception is not caught by a nested interceptor). 83 | #[allow( 84 | clippy::missing_errors_doc, 85 | reason = "`Err` value is described immediately" 86 | )] 87 | fn intercept R, R>(func: Func) -> Result; 88 | } 89 | 90 | /// Throw-by-value backend. 91 | /// 92 | /// Implementors of this trait should consider exceptions as generic objects. Any additional 93 | /// information used by the implementor has to be stored separately. 94 | /// 95 | /// # Safety 96 | /// 97 | /// Implementations must satisfy the rules of the "Safety" section of [this module](self). In 98 | /// addition: 99 | /// 100 | /// The implementation may modify the header arbitrarily during unwinding, but modifying the 101 | /// exception object is forbidden. 102 | /// 103 | /// If the `intercept` method returns `Err`, the returned value must be the same as the value passed 104 | /// to `throw`. 105 | pub unsafe trait ThrowByValue { 106 | /// A [`RethrowHandle`]. 107 | type RethrowHandle: RethrowHandle; 108 | 109 | /// Throw an exception. 110 | /// 111 | /// # Safety 112 | /// 113 | /// It is important that intermediate call frames don't preclude unwinding from happening 114 | /// soundly. For example, [`catch_unwind`](std::panic::catch_unwind) can safely catch panics and 115 | /// may start catching foreign exceptions soon, both of which can confuse the user of this 116 | /// trait. 117 | /// 118 | /// For this reason, the caller must ensure no intermediate frames can affect unwinding. This 119 | /// includes not passing throwing callbacks to foreign crates, but also not using `throw` in own 120 | /// code that might `intercept` an exception without cooperation with the throwing side. 121 | unsafe fn throw(cause: E) -> !; 122 | 123 | /// Catch an exception. 124 | /// 125 | /// This function returns `Ok` if the function returns normally, or `Err` if it throws (and the 126 | /// thrown exception is not caught by a nested interceptor). 127 | /// 128 | /// # Safety 129 | /// 130 | /// The type `E` must match the type of the thrown exception. 131 | /// 132 | /// In addition, certain requirements are imposed on how the returned [`RethrowHandle`] is used. 133 | /// In particular, no exceptions may be thrown between the moment this function returns and the 134 | /// moment the handle is dropped (either by calling [`drop`] or by calling its 135 | /// [`RethrowHandle::rethrow`] method). Panics, however, are allowed, as are caught exceptions. 136 | #[allow( 137 | clippy::missing_errors_doc, 138 | reason = "`Err` value is described immediately" 139 | )] 140 | unsafe fn intercept R, R, E>( 141 | func: Func, 142 | ) -> Result)>; 143 | } 144 | 145 | /// A rethrow handle. 146 | /// 147 | /// This handle is returned by [`ThrowByValue::intercept`] implementations that support efficient 148 | /// rethrowing. Sometimes, certain allocations or structures can be retained between throw calls, 149 | /// and this handle can be used to optimize this. 150 | /// 151 | /// The handle owns the structures/allocations, and when it's dropped, it should free those 152 | /// resources, if necessary. 153 | pub trait RethrowHandle { 154 | /// Throw a new exception by reusing the existing context. 155 | /// 156 | /// See [`ThrowByValue::intercept`] docs for examples and safety notes. 157 | /// 158 | /// # Safety 159 | /// 160 | /// All safety requirements of [`ThrowByValue::throw`] apply. 161 | unsafe fn rethrow(self, new_cause: F) -> !; 162 | } 163 | 164 | #[cfg(backend = "itanium")] 165 | #[path = "itanium.rs"] 166 | mod imp; 167 | 168 | #[cfg(backend = "seh")] 169 | #[path = "seh.rs"] 170 | mod imp; 171 | 172 | #[cfg(backend = "panic")] 173 | #[path = "panic.rs"] 174 | mod imp; 175 | 176 | #[cfg(backend = "emscripten")] 177 | #[path = "emscripten.rs"] 178 | mod imp; 179 | 180 | #[cfg(backend = "unimplemented")] 181 | #[path = "unimplemented.rs"] 182 | mod imp; 183 | 184 | pub(crate) use imp::ActiveBackend; 185 | 186 | #[cfg(test)] 187 | mod test { 188 | use super::{ActiveBackend, RethrowHandle, ThrowByValue}; 189 | use alloc::string::String; 190 | 191 | #[test] 192 | fn intercept_ok() { 193 | let result = 194 | unsafe { ActiveBackend::intercept::<_, _, ()>(|| String::from("Hello, world!")) }; 195 | assert_eq!(result.unwrap(), "Hello, world!"); 196 | } 197 | 198 | #[test] 199 | fn intercept_err() { 200 | let result = unsafe { 201 | ActiveBackend::intercept::<_, _, String>(|| { 202 | ActiveBackend::throw(String::from("Hello, world!")); 203 | }) 204 | }; 205 | let (caught_ex, _) = result.unwrap_err(); 206 | assert_eq!(caught_ex, "Hello, world!"); 207 | } 208 | 209 | #[test] 210 | fn intercept_panic() { 211 | let result = std::panic::catch_unwind(|| unsafe { 212 | ActiveBackend::intercept::<_, _, ()>(|| { 213 | std::panic::resume_unwind(alloc::boxed::Box::new("Hello, world!")) 214 | }) 215 | }); 216 | assert_eq!( 217 | *result.unwrap_err().downcast_ref::<&'static str>().unwrap(), 218 | "Hello, world!", 219 | ); 220 | } 221 | 222 | #[test] 223 | fn nested_intercept() { 224 | let result = unsafe { 225 | ActiveBackend::intercept::<_, _, ()>(|| { 226 | ActiveBackend::intercept::<_, _, String>(|| { 227 | ActiveBackend::throw(String::from("Hello, world!")); 228 | }) 229 | }) 230 | }; 231 | let (caught_ex, _) = result.unwrap().unwrap_err(); 232 | assert_eq!(caught_ex, "Hello, world!"); 233 | } 234 | 235 | #[test] 236 | fn rethrow() { 237 | let result = unsafe { 238 | ActiveBackend::intercept::<_, _, String>(|| { 239 | let result = ActiveBackend::intercept::<_, _, String>(|| { 240 | ActiveBackend::throw(String::from("Hello, world!")); 241 | }); 242 | let (ex2, handle) = result.unwrap_err(); 243 | assert_eq!(ex2, "Hello, world!"); 244 | handle.rethrow(ex2); 245 | }) 246 | }; 247 | let (caught_ex, _) = result.unwrap_err(); 248 | assert_eq!(caught_ex, "Hello, world!"); 249 | } 250 | 251 | #[test] 252 | fn destructors_are_run() { 253 | struct Dropper<'a>(&'a mut bool); 254 | impl Drop for Dropper<'_> { 255 | fn drop(&mut self) { 256 | *self.0 = true; 257 | } 258 | } 259 | 260 | let mut destructor_was_run = false; 261 | let result = unsafe { 262 | ActiveBackend::intercept::<_, _, String>(|| { 263 | let _dropper = Dropper(&mut destructor_was_run); 264 | ActiveBackend::throw(String::from("Hello, world!")); 265 | }) 266 | }; 267 | let (caught_ex, _) = result.unwrap_err(); 268 | assert_eq!(caught_ex, "Hello, world!"); 269 | 270 | assert!(destructor_was_run); 271 | } 272 | 273 | #[test] 274 | fn nested_with_drop() { 275 | struct Dropper; 276 | impl Drop for Dropper { 277 | fn drop(&mut self) { 278 | let result = unsafe { 279 | ActiveBackend::intercept::<_, _, String>(|| { 280 | ActiveBackend::throw(String::from("Awful idea")); 281 | }) 282 | }; 283 | let (caught_ex2, _) = result.unwrap_err(); 284 | assert_eq!(caught_ex2, "Awful idea"); 285 | } 286 | } 287 | 288 | let result = unsafe { 289 | ActiveBackend::intercept::<_, _, String>(|| { 290 | let _dropper = Dropper; 291 | ActiveBackend::throw(String::from("Hello, world!")); 292 | }) 293 | }; 294 | let (caught_ex1, _) = result.unwrap_err(); 295 | assert_eq!(caught_ex1, "Hello, world!"); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/backend/panic.rs: -------------------------------------------------------------------------------- 1 | use super::ThrowByPointer; 2 | use alloc::boxed::Box; 3 | use core::mem::ManuallyDrop; 4 | use core::panic::AssertUnwindSafe; 5 | use std::panic::{catch_unwind, resume_unwind}; 6 | 7 | pub(crate) struct ActiveBackend; 8 | 9 | /// A generic panic-powered backend. 10 | /// 11 | /// This backend uses a more generic method than Itanium: stable, cross-platform, and available 12 | /// whenever `panic = "unwind"` is enabled. It is less efficient than throwing exceptions manually, 13 | /// but it's the next best thing. 14 | /// 15 | /// What we can't affect performance-wise is the allocation of an `_Unwind_Exception` (at least on 16 | /// Itanium), performed inside `std` with `Box`. What we *do* want to avoid is the allocation of 17 | /// `Box`, which stores the panic payload. 18 | /// 19 | /// Implementation-wise, the idea is simple. An unsized box stores two words, one used for data and 20 | /// one for RTTI. We can supply a unique value for the RTTI word to be able to recognize our panics, 21 | /// and the data word can just contain the thrown `*mut ExceptionHeader`. 22 | /// 23 | /// Luckily, the "Memory layout" section of `Box` docs specifies that only boxes wrapping non-ZST 24 | /// types need to have been allocated by the `Global` allocator, so we aren't even comitting UB as 25 | /// long as we're throwing `Box::::from_raw(ex)`, as long as `ExceptionHeader` is 26 | /// a ZST. 27 | /// 28 | /// The devil, however, is in the details. From the AM point of view, 29 | /// `Box::into_raw(Box::from_raw(ex))` is not a no-op: it enforces uniqueness, which is performed 30 | /// differently under Stacked Borrows and Tree Borrows. Under TB, this is not a problem and our 31 | /// approach is sound. 32 | /// 33 | /// Under SB, however, `Box::from_raw` reduces the provenance of the passed pointer to just 34 | /// `ExceptionHeader`, losing information about the surrounding object; thus accessing the original 35 | /// exception after `Box::into_raw` is UB. This is [a well-known deficiency][1] in SB, fixed by TB. 36 | /// 37 | /// As far as I am aware, rustc does not use this SB unsoundness for optimizations, so this approach 38 | /// will not cause problems in practical code. So the only question is: what should we do under SB? 39 | /// The obvious approach is to keep the UB, but as Miri stops simulation on UB, this might shadow 40 | /// bugs we're actually interested in; in fact, it might hide bugs in user code when our downstream 41 | /// depenedncies use Miri. 42 | /// 43 | /// So instead, we use a very similar approach based on exposed provenance. This is not UB under SB, 44 | /// but it cause deoptimizations elsewhere, so we only enable it conditionally. Pulling a Volkswagen 45 | /// is not something to be proud of, but at least we don't cheat under TB. If this approach turns 46 | /// out to not lead to deoptimizations in practice, we might enable it unconditionally. 47 | /// 48 | /// [1]: https://github.com/rust-lang/unsafe-code-guidelines/issues/256 49 | // SAFETY: We basically use Rust's own mechanism for unwinding (panics), which satisfies all 50 | // requirements. 51 | unsafe impl ThrowByPointer for ActiveBackend { 52 | type ExceptionHeader = LithiumMarker; 53 | 54 | fn new_header() -> LithiumMarker { 55 | LithiumMarker 56 | } 57 | 58 | #[inline] 59 | unsafe fn throw(ex: *mut LithiumMarker) -> ! { 60 | #[cfg(feature = "sound-under-stacked-borrows")] 61 | ex.expose_provenance(); 62 | // SAFETY: `LithiumMarker` is a ZST, so casting the pointer to a box is safe as long as the 63 | // pointer is aligned and valid, which it is by the safety requirements of this function. 64 | let ex = unsafe { Box::from_raw(ex) }; 65 | resume_unwind(ex); 66 | } 67 | 68 | #[inline(always)] 69 | fn intercept R, R>(func: Func) -> Result { 70 | catch_unwind(AssertUnwindSafe(func)).map_err(|ex| { 71 | // Rust does not know that `is` cannot unwind and attempts to generate a cleanup pad for 72 | // `ex`. This increases code size, and it's bad because `intercept` is inlined into 73 | // many callers. Fix this by temporarily storing `ex` in `ManuallyDrop`. 74 | let ex = ManuallyDrop::new(ex); 75 | let is_marker = ex.is::(); 76 | let ex = ManuallyDrop::into_inner(ex); 77 | if is_marker { 78 | // If this is a `LithiumMarker`, it must have been produced by `throw`, because this 79 | // type is crate-local and we don't use it elsewhere. The safety requirements for 80 | // `throw` require no messing with unwinding up to `intercept`, so this must have 81 | // been our exception. 82 | let ex: *mut LithiumMarker = Box::into_raw(ex).cast(); 83 | #[cfg(feature = "sound-under-stacked-borrows")] 84 | let ex = core::ptr::with_exposed_provenance_mut(ex.addr()); 85 | ex 86 | } else { 87 | // If this isn't `LithiumMarker`, it can't be thrown by us, so no exceptions are 88 | // lost. 89 | resume_unwind(ex); 90 | } 91 | }) 92 | } 93 | } 94 | 95 | pub(crate) struct LithiumMarker; 96 | -------------------------------------------------------------------------------- /src/backend/seh.rs: -------------------------------------------------------------------------------- 1 | // This is partially taken from 2 | // - https://github.com/rust-lang/rust/blob/master/library/panic_unwind/src/seh.rs 3 | // with exception constants and the throwing interface retrieved from ReactOS and Wine sources. 4 | 5 | use super::{ 6 | super::{abort, intrinsic::intercept}, 7 | RethrowHandle, ThrowByValue, 8 | }; 9 | use alloc::boxed::Box; 10 | use core::any::Any; 11 | use core::marker::{FnPtr, PhantomData}; 12 | use core::mem::ManuallyDrop; 13 | use core::panic::PanicPayload; 14 | use core::sync::atomic::{AtomicU32, Ordering}; 15 | 16 | pub(crate) struct ActiveBackend; 17 | 18 | /// SEH-based unwinding. 19 | /// 20 | /// Just like with Itanium, we piggy-back on the [`core::intrinsics::catch_unwind`] intrinsic. 21 | /// Currently, it's configured to catch C++ exceptions with mangled type `rust_panic`, so that's the 22 | /// kind of exception we have to throw. 23 | /// 24 | /// This means that we'll also catch Rust panics, so we need to be able to separate them from our 25 | /// exceptions. Luckily, Rust already puts a `canary` field in the exception object to check if it's 26 | /// caught an exception by another Rust std; we'll use it for our own purposes by providing a unique 27 | /// canary value. 28 | /// 29 | /// SEH has its share of problems, but one cool detail is that stack is not unwinded until the catch 30 | /// handler returns. This means that we can save the exception object on stack and then copy it to 31 | /// the destination from the catch handler, thus reducing allocations. 32 | // SAFETY: SEH satisfies the requirements. 33 | unsafe impl ThrowByValue for ActiveBackend { 34 | type RethrowHandle = SehRethrowHandle; 35 | 36 | #[inline(always)] 37 | unsafe fn throw(cause: E) -> ! { 38 | // We have to initialize these variables late because we can't ask the linker to do the 39 | // relative address computation for us. Using atomics for this removes races in Rust code, 40 | // but atomic writes can still race with non-atomic reads in the vcruntime code. Luckily, we 41 | // aren't going to LTO with vcruntime. 42 | CATCHABLE_TYPE 43 | .type_descriptor 44 | .write(SmallPtr::new(&raw const TYPE_DESCRIPTOR)); 45 | CATCHABLE_TYPE.copy_function.write(SmallPtr::new_fn(copy)); 46 | CATCHABLE_TYPE_ARRAY.catchable_types[0].write(SmallPtr::new(&raw const CATCHABLE_TYPE)); 47 | THROW_INFO.destructor.write(SmallPtr::new_fn(cleanup)); 48 | THROW_INFO 49 | .catchable_type_array 50 | .write(SmallPtr::new(&raw const CATCHABLE_TYPE_ARRAY)); 51 | 52 | // SAFETY: We've just initialized the tables. 53 | unsafe { 54 | do_throw(cause); 55 | } 56 | } 57 | 58 | #[inline(always)] 59 | unsafe fn intercept R, R, E>(func: Func) -> Result { 60 | enum CaughtUnwind { 61 | LithiumException(E), 62 | RustPanic(Box), 63 | } 64 | 65 | let catch = |ex: *mut u8| { 66 | // This callback is not allowed to unwind, so we can't rethrow exceptions. 67 | if ex.is_null() { 68 | // This is a foreign exception. 69 | abort( 70 | "Lithium caught a foreign exception. This is unsupported. The process will now terminate.\n", 71 | ); 72 | } 73 | 74 | let ex_lithium: *mut Exception = ex.cast(); 75 | 76 | // SAFETY: If `ex` is non-null, it's a `rust_panic` exception, which can either be 77 | // thrown by us or by the Rust runtime; both have the `header.canary` field as the first 78 | // field in their structures. 79 | if unsafe { (*ex_lithium).header.canary } != (&raw const THROW_INFO).cast() { 80 | // This is a Rust exception. We can't rethrow it immediately from this nounwind 81 | // callback, so let's catch it first. 82 | // SAFETY: `ex` is the callback value of `core::intrinsics::catch_unwind`. 83 | let payload = unsafe { __rust_panic_cleanup(ex) }; 84 | // SAFETY: `__rust_panic_cleanup` returns a Box. 85 | let payload = unsafe { Box::from_raw(payload) }; 86 | return CaughtUnwind::RustPanic(payload); 87 | } 88 | 89 | // We catch the exception by reference, so the C++ runtime will drop it. Tell our 90 | // destructor to calm down. 91 | // SAFETY: This is our exception, so `ex_lithium` points at a valid instance of 92 | // `Exception`. 93 | unsafe { 94 | (*ex_lithium).header.caught = true; 95 | } 96 | // SAFETY: As above. 97 | let cause = unsafe { &mut (*ex_lithium).cause }; 98 | // SAFETY: We only read the cause here, so no double copies. 99 | CaughtUnwind::LithiumException(unsafe { ManuallyDrop::take(cause) }) 100 | }; 101 | 102 | match intercept(func, catch) { 103 | Ok(value) => Ok(value), 104 | Err(CaughtUnwind::LithiumException(cause)) => Err((cause, SehRethrowHandle)), 105 | Err(CaughtUnwind::RustPanic(payload)) => throw_std_panic(payload), 106 | } 107 | } 108 | } 109 | 110 | #[derive(Debug)] 111 | pub(crate) struct SehRethrowHandle; 112 | 113 | impl RethrowHandle for SehRethrowHandle { 114 | #[inline(never)] 115 | unsafe fn rethrow(self, new_cause: F) -> ! { 116 | // SAFETY: This is a rethrow, so the first throw must have initialized the tables. 117 | unsafe { 118 | do_throw(new_cause); 119 | } 120 | } 121 | } 122 | 123 | /// Throw an exception as a C++ exception. 124 | /// 125 | /// # Safety 126 | /// 127 | /// The caller must ensure all global tables are initialized. 128 | unsafe fn do_throw(cause: E) -> ! { 129 | let mut exception = Exception { 130 | header: ExceptionHeader { 131 | canary: (&raw const THROW_INFO).cast(), // any static will work 132 | caught: false, 133 | }, 134 | cause: ManuallyDrop::new(cause), 135 | }; 136 | 137 | // SAFETY: THROW_INFO exists for the whole duration of the program. 138 | unsafe { 139 | cxx_throw((&raw mut exception).cast(), &raw const THROW_INFO); 140 | } 141 | } 142 | 143 | #[repr(C)] 144 | struct ExceptionHeader { 145 | canary: *const (), // From Rust ABI 146 | caught: bool, 147 | } 148 | 149 | #[repr(C)] 150 | struct Exception { 151 | header: ExceptionHeader, 152 | cause: ManuallyDrop, 153 | } 154 | 155 | #[cfg(target_arch = "x86")] 156 | macro_rules! thiscall { 157 | ($(#[$outer:meta])* fn $($tt:tt)*) => { 158 | $(#[$outer])* unsafe extern "thiscall" fn $($tt)* 159 | }; 160 | } 161 | #[cfg(not(target_arch = "x86"))] 162 | macro_rules! thiscall { 163 | ($(#[$outer:meta])* fn $($tt:tt)*) => { 164 | $(#[$outer])* unsafe extern "C" fn $($tt)* 165 | }; 166 | } 167 | 168 | #[repr(C)] 169 | struct ExceptionRecordParameters { 170 | magic: usize, 171 | exception_object: *mut ExceptionHeader, 172 | throw_info: *const ThrowInfo, 173 | #[cfg(target_pointer_width = "64")] 174 | image_base: *const u8, 175 | } 176 | 177 | #[repr(C)] 178 | struct ThrowInfo { 179 | attributes: u32, 180 | destructor: SmallPtr, 181 | forward_compat: SmallPtr, 182 | catchable_type_array: SmallPtr<*const CatchableTypeArray>, 183 | } 184 | 185 | #[repr(C)] 186 | struct CatchableTypeArray { 187 | n_types: i32, 188 | catchable_types: [SmallPtr<*const CatchableType>; 1], 189 | } 190 | 191 | #[repr(C)] 192 | struct CatchableType { 193 | properties: u32, 194 | type_descriptor: SmallPtr<*const TypeDescriptor>, 195 | this_displacement: PointerToMemberData, 196 | size_or_offset: i32, 197 | copy_function: SmallPtr< 198 | thiscall!(fn(*mut ExceptionHeader, *const ExceptionHeader) -> *mut ExceptionHeader), 199 | >, 200 | } 201 | 202 | #[repr(C)] 203 | struct TypeDescriptor { 204 | vtable: *const *const (), 205 | reserved: usize, 206 | name: [u8; 11], // null-terminated 207 | } 208 | // SAFETY: `!Sync` for pointers is stupid. 209 | unsafe impl Sync for TypeDescriptor {} 210 | 211 | #[repr(C)] 212 | struct PointerToMemberData { 213 | member_displacement: i32, 214 | virtual_base_pointer_displacement: i32, 215 | vdisp: i32, // ??? 216 | } 217 | 218 | // See ehdata.h for definitions 219 | const EH_EXCEPTION_NUMBER: u32 = u32::from_be_bytes(*b"\xe0msc"); 220 | const EH_NONCONTINUABLE: u32 = 1; 221 | const EH_MAGIC_NUMBER1: usize = 0x1993_0520; // Effectively a version 222 | 223 | static TYPE_DESCRIPTOR: TypeDescriptor = TypeDescriptor { 224 | vtable: &raw const TYPE_INFO_VTABLE, 225 | reserved: 0, 226 | name: *b"rust_panic\0", 227 | }; 228 | 229 | static CATCHABLE_TYPE: CatchableType = CatchableType { 230 | properties: 0, 231 | type_descriptor: SmallPtr::null(), // filled by throw 232 | this_displacement: PointerToMemberData { 233 | member_displacement: 0, 234 | virtual_base_pointer_displacement: -1, 235 | vdisp: 0, 236 | }, 237 | // We don't really have a good answer to this, and we don't let the C++ runtime catch our 238 | // exception, so it's not a big problem. 239 | size_or_offset: 1, 240 | copy_function: SmallPtr::null(), // filled by throw 241 | }; 242 | 243 | static CATCHABLE_TYPE_ARRAY: CatchableTypeArray = CatchableTypeArray { 244 | n_types: 1, 245 | catchable_types: [ 246 | SmallPtr::null(), // filled by throw 247 | ], 248 | }; 249 | 250 | static THROW_INFO: ThrowInfo = ThrowInfo { 251 | attributes: 0, 252 | destructor: SmallPtr::null(), // filled by throw 253 | forward_compat: SmallPtr::null(), 254 | catchable_type_array: SmallPtr::null(), // filled by throw 255 | }; 256 | 257 | fn abort_on_caught_by_cxx() -> ! { 258 | abort("A Lithium exception was caught by a non-Lithium catch mechanism. This is undefined behavior. The process will now terminate.\n"); 259 | } 260 | 261 | thiscall! { 262 | /// Destruct an exception object. 263 | /// 264 | /// # Safety 265 | /// 266 | /// `ex` must point at a valid exception object. 267 | fn cleanup(ex: *mut ExceptionHeader) { 268 | // SAFETY: `ex` is a `this` pointer when called by the C++ runtime. 269 | if !unsafe { (*ex).caught } { 270 | // Caught by the cxx runtime 271 | abort_on_caught_by_cxx(); 272 | } 273 | } 274 | } 275 | 276 | thiscall! { 277 | /// Copy an exception object. 278 | /// 279 | /// # Safety 280 | /// 281 | /// `from` must point at a valid exception object, while `to` must point at a suitable 282 | /// allocation for the new object. 283 | fn copy(_to: *mut ExceptionHeader, _from: *const ExceptionHeader) -> *mut ExceptionHeader { 284 | abort_on_caught_by_cxx(); 285 | } 286 | } 287 | 288 | extern "C" { 289 | #[cfg(target_pointer_width = "64")] 290 | static __ImageBase: u8; 291 | 292 | #[link_name = "\x01??_7type_info@@6B@"] 293 | static TYPE_INFO_VTABLE: *const (); 294 | } 295 | 296 | #[repr(transparent)] 297 | struct SmallPtr

{ 298 | value: AtomicU32, 299 | phantom: PhantomData

, 300 | } 301 | 302 | // SAFETY: `!Sync` for pointers is stupid. 303 | unsafe impl

Sync for SmallPtr

{} 304 | 305 | impl

SmallPtr

{ 306 | /// Construct a small pointer. 307 | /// 308 | /// # Panics 309 | /// 310 | /// Panics if `p` is too far from the image base. 311 | #[inline] 312 | fn from_erased(p: *const ()) -> Self { 313 | #[cfg(target_pointer_width = "32")] 314 | let value = p.expose_provenance() as u32; 315 | #[cfg(target_pointer_width = "64")] 316 | #[expect( 317 | clippy::cast_possible_truncation, 318 | reason = "PE images are at most 4 GiB long" 319 | )] 320 | let value = p 321 | .expose_provenance() 322 | .wrapping_sub((&raw const __ImageBase).addr()) as u32; 323 | Self { 324 | value: AtomicU32::new(value), 325 | phantom: PhantomData, 326 | } 327 | } 328 | 329 | const fn null() -> Self { 330 | Self { 331 | value: AtomicU32::new(0), 332 | phantom: PhantomData, 333 | } 334 | } 335 | 336 | fn write(&self, rhs: SmallPtr

) { 337 | self.value.store(rhs.value.into_inner(), Ordering::Relaxed); 338 | } 339 | } 340 | 341 | impl SmallPtr

{ 342 | fn new_fn(p: P) -> Self { 343 | Self::from_erased(p.addr()) 344 | } 345 | } 346 | 347 | impl SmallPtr<*const T> { 348 | fn new(p: *const T) -> Self { 349 | Self::from_erased(p.cast()) 350 | } 351 | } 352 | 353 | extern "system-unwind" { 354 | fn RaiseException( 355 | code: u32, 356 | flags: u32, 357 | n_parameters: u32, 358 | paremeters: *mut ExceptionRecordParameters, 359 | ) -> !; 360 | } 361 | 362 | // This is provided by the `panic_unwind` built-in crate, so it's always available if 363 | // panic = "unwind" holds 364 | extern "Rust" { 365 | fn __rust_start_panic(payload: &mut dyn PanicPayload) -> u32; 366 | } 367 | 368 | extern "C" { 369 | #[expect(improper_ctypes, reason = "Copied from std")] 370 | fn __rust_panic_cleanup(payload: *mut u8) -> *mut (dyn Any + Send + 'static); 371 | } 372 | 373 | fn throw_std_panic(payload: Box) -> ! { 374 | // We can't use resume_unwind here, as it increments the panic count, and we didn't decrement it 375 | // upon catching the panic. Call `__rust_start_panic` directly instead. 376 | struct RewrapBox(Box); 377 | 378 | // SAFETY: Copied straight from std. 379 | unsafe impl PanicPayload for RewrapBox { 380 | fn take_box(&mut self) -> *mut (dyn Any + Send) { 381 | Box::into_raw(core::mem::replace(&mut self.0, Box::new(()))) 382 | } 383 | fn get(&mut self) -> &(dyn Any + Send) { 384 | &*self.0 385 | } 386 | } 387 | 388 | impl core::fmt::Display for RewrapBox { 389 | fn fmt(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 390 | // `__rust_start_panic` is not supposed to use the `Display` implementation in unwinding 391 | // mode. 392 | unreachable!() 393 | } 394 | } 395 | 396 | // SAFETY: Copied straight from std. 397 | unsafe { 398 | __rust_start_panic(&mut RewrapBox(payload)); 399 | } 400 | core::intrinsics::abort(); 401 | } 402 | 403 | /// Throw a C++ exception. 404 | /// 405 | /// # Safety 406 | /// 407 | /// `throw_info` must point to a correctly initialized `ThrowInfo` value, valid for the whole 408 | /// duration of the unwinding procedure. 409 | #[inline(always)] 410 | unsafe fn cxx_throw(exception_object: *mut ExceptionHeader, throw_info: *const ThrowInfo) -> ! { 411 | // This is a reimplementation of `_CxxThrowException`, with quite a few information hardcoded 412 | // and functions calls inlined. 413 | 414 | #[expect(clippy::cast_possible_truncation, reason = "This is a constant")] 415 | const N_PARAMETERS: u32 = 416 | (core::mem::size_of::() / core::mem::size_of::()) as u32; 417 | 418 | let mut parameters = ExceptionRecordParameters { 419 | magic: EH_MAGIC_NUMBER1, 420 | exception_object, 421 | throw_info, 422 | #[cfg(target_pointer_width = "64")] 423 | image_base: &raw const __ImageBase, 424 | }; 425 | 426 | // SAFETY: Just an extern call. 427 | unsafe { 428 | RaiseException( 429 | EH_EXCEPTION_NUMBER, 430 | EH_NONCONTINUABLE, 431 | N_PARAMETERS, 432 | &raw mut parameters, 433 | ); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/backend/unimplemented.rs: -------------------------------------------------------------------------------- 1 | use super::{RethrowHandle, ThrowByValue}; 2 | 3 | pub(crate) struct ActiveBackend; 4 | 5 | compile_error!("Lithium does not support builds without std on this platform"); 6 | 7 | unsafe impl ThrowByValue for ActiveBackend { 8 | type RethrowHandle = UnimplementedRethrowHandle; 9 | 10 | unsafe fn throw(_cause: E) -> ! { 11 | unimplemented!() 12 | } 13 | 14 | unsafe fn intercept R, R, E>( 15 | _func: Func, 16 | ) -> Result)> { 17 | unimplemented!() 18 | } 19 | } 20 | 21 | #[derive(Debug)] 22 | pub(crate) struct UnimplementedRethrowHandle; 23 | 24 | impl RethrowHandle for UnimplementedRethrowHandle { 25 | unsafe fn rethrow(self, _new_cause: F) -> ! { 26 | unimplemented!() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/heterogeneous_stack/align.rs: -------------------------------------------------------------------------------- 1 | /// Asserts that `n` is a multiple of `align_of::()`. 2 | /// 3 | /// # Panics 4 | /// 5 | /// Panics if `n` is not a multiple of alignment. 6 | pub fn assert_aligned(n: usize) { 7 | #[expect( 8 | clippy::arithmetic_side_effects, 9 | reason = "The divisor is never 0 and we're working in unsigned" 10 | )] 11 | let modulo = n % align_of::(); 12 | assert!(modulo == 0, "Unaligned"); 13 | } 14 | 15 | #[cfg(test)] 16 | mod test { 17 | use super::*; 18 | 19 | #[test] 20 | fn pass() { 21 | assert_aligned::(0); 22 | assert_aligned::(8); 23 | } 24 | 25 | #[test] 26 | #[should_panic] 27 | fn fail() { 28 | assert_aligned::(3); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/heterogeneous_stack/array.rs: -------------------------------------------------------------------------------- 1 | use super::align::assert_aligned; 2 | use core::cell::{Cell, UnsafeCell}; 3 | use core::mem::MaybeUninit; 4 | 5 | /// A thread-unsafe array-backed stack allocator. 6 | /// 7 | /// This allocator can allocate values with sizes that are multiples of `align_of::()`, 8 | /// guaranteeing alignment to `align_of::()`. 9 | /// 10 | /// The allocated bytes are always consecutive. 11 | // Safety invariants: 12 | // - len is a factor of `align_of::()` 13 | // - `len <= CAPACITY` 14 | // - References to `data[len..]` are not used; `data[..len]` may be arbitrarily referenced 15 | // - All elements are consecutive, with the last element ending at `len` 16 | // - Element sizes are multiples of `align_of::()` 17 | // - Allocation necessarily succeeds if there is enough capacity left 18 | #[repr(C)] 19 | pub struct Stack { 20 | _align: [AlignAs; 0], 21 | data: UnsafeCell<[MaybeUninit; CAPACITY]>, 22 | pub(super) len: Cell, 23 | } 24 | 25 | impl Stack { 26 | /// Create an empty stack. 27 | pub const fn new() -> Self { 28 | Self { 29 | len: Cell::new(0), 30 | _align: [], 31 | data: UnsafeCell::new([MaybeUninit::uninit(); CAPACITY]), 32 | } 33 | } 34 | 35 | /// Allocate `n` bytes. 36 | /// 37 | /// The returned pointer is guaranteed to be aligned to `align_of::()` and valid for 38 | /// reads/writes for `n` bytes. It is also guaranteed to be unique. 39 | /// 40 | /// Returns `None` if there isn't enough space. It is guaranteed that allocation always succeeds 41 | /// if there's at least `n` free capacity. In particular, allocating 0 bytes always succeeds. 42 | /// 43 | /// # Panics 44 | /// 45 | /// Panics if `n` is not a multiple of `align_of::()`. 46 | pub fn try_push(&self, n: usize) -> Option<*mut u8> { 47 | assert_aligned::(n); 48 | 49 | if n == 0 { 50 | // Dangling pointers to ZSTs are always valid and unique. Creating `*mut AlignAs` 51 | // instead of *mut u8` forces alignment. 52 | return Some(core::ptr::dangling_mut::().cast()); 53 | } 54 | 55 | // SAFETY: len <= CAPACITY is an invariant 56 | let capacity_left = unsafe { CAPACITY.unchecked_sub(self.len.get()) }; 57 | if n > capacity_left { 58 | // Type invariant: not enough capacity left 59 | return None; 60 | } 61 | 62 | // SAFETY: len is in-bounds for data by the invariant 63 | let ptr = unsafe { self.data.get().byte_add(self.len.get()) }; 64 | 65 | // - `ptr` is aligned because both `data` and `len` are aligned 66 | // - `ptr` is valid for reads/writes for `n` bytes because it's a subset of an allocation 67 | // - `ptr` is unique by the type invariant 68 | let ptr: *mut u8 = ptr.cast(); 69 | 70 | // SAFETY: n <= capacity - len implies len + n <= capacity < usize::MAX 71 | self.len.set(unsafe { self.len.get().unchecked_add(n) }); 72 | 73 | // Type invariants: 74 | // - len' is a factor of align_of::(), as n is a factor of alignment 75 | // - len' <= CAPACITY still holds 76 | // - References to data[len'..] are not used by the invariant as len' >= len 77 | // - The new element is located immediately at len with no empty space, len' is minimal 78 | Some(ptr) 79 | } 80 | 81 | /// Remove `n` bytes from the top of the stack. 82 | /// 83 | /// # Panics 84 | /// 85 | /// Panics if `n` is not a multiple of `align_of::()`. 86 | /// 87 | /// # Safety 88 | /// 89 | /// The caller must ensure that: 90 | /// - The stack has at least `n` bytes allocated. 91 | /// - References to the top `n` bytes, both immutable or mutable, are not used after 92 | /// `pop_unchecked` is called. 93 | pub unsafe fn pop_unchecked(&self, n: usize) { 94 | assert_aligned::(n); 95 | 96 | // For ZSTs, this is a no-op. 97 | // SAFETY: len >= n by the safety requirement 98 | self.len.set(unsafe { self.len.get().unchecked_sub(n) }); 99 | 100 | // Type invariants: 101 | // - len' is a factor of align_of::(), as n is a factor of alignment 102 | // - len' <= len <= CAPACITY holds 103 | // - References to data[len'..len] are not used by the safety requirement 104 | // - The previous allocation ends at len' 105 | } 106 | 107 | /// Check whether an allocation is within the stack. 108 | /// 109 | /// If `ptr` was produced from allocating `n` bytes with this stack and the stack hasn't been 110 | /// moved since the allocation, this returns `true`. 111 | /// 112 | /// If `ptr` was produced by another allocator that couldn't have used the stack space 113 | /// **and `n > 0`**, this returns `false`. 114 | /// 115 | /// If `n = 0`, this always returns `true`, ignoring the pointer. 116 | /// 117 | /// In all other cases, the return value is unspecified. 118 | pub fn contains_allocated(&self, ptr: *const u8, n: usize) -> bool { 119 | if n == 0 { 120 | return true; 121 | } 122 | // Types larger than CAPACITY can never be successfully allocated 123 | CAPACITY.checked_sub(n).is_some_and(|limit| { 124 | // For non-ZSTs, stack-allocated pointers addresses are within 125 | // [data; data + CAPACITY - n], and this region cannot intersect with non-ZSTs 126 | // allocated by other methods. 127 | ptr.addr().wrapping_sub(self.data.get().addr()) <= limit 128 | }) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod test { 134 | use super::*; 135 | use alloc::boxed::Box; 136 | 137 | #[test] 138 | #[should_panic] 139 | fn unaligned_push() { 140 | Stack::::new().try_push(3); 141 | } 142 | 143 | #[test] 144 | #[should_panic] 145 | fn unaligned_pop() { 146 | unsafe { 147 | Stack::::new().pop_unchecked(1); 148 | } 149 | } 150 | 151 | #[test] 152 | fn overaligned() { 153 | #[repr(align(256))] 154 | struct Overaligned; 155 | let stack = Stack::::new(); 156 | let ptr = stack.try_push(256).expect("failed to allocate"); 157 | assert_eq!(ptr.addr() % 256, 0); 158 | } 159 | 160 | #[test] 161 | fn consecutive() { 162 | let stack = Stack::::new(); 163 | assert_eq!(stack.len.get(), 0); 164 | let ptr1 = stack.try_push(5).expect("failed to allocate"); 165 | assert_eq!(stack.len.get(), 5); 166 | let ptr2 = stack.try_push(8).expect("failed to allocate"); 167 | assert_eq!(stack.len.get(), 13); 168 | let ptr3 = stack.try_push(1).expect("failed to allocate"); 169 | assert_eq!(stack.len.get(), 14); 170 | assert_eq!(ptr2.addr() - ptr1.addr(), 5); 171 | assert_eq!(ptr3.addr() - ptr2.addr(), 8); 172 | unsafe { stack.pop_unchecked(1) }; 173 | assert_eq!(stack.len.get(), 13); 174 | let ptr4 = stack.try_push(2).expect("failed to allocate"); 175 | assert_eq!(ptr3.addr(), ptr4.addr()); 176 | } 177 | 178 | #[test] 179 | fn too_large() { 180 | let stack = Stack::::new(); 181 | stack.try_push(5); 182 | assert!(stack.try_push(12).is_none(), "allocation fit"); 183 | } 184 | 185 | #[test] 186 | fn pop_zero() { 187 | let stack = Stack::::new(); 188 | unsafe { 189 | stack.pop_unchecked(0); 190 | } 191 | } 192 | 193 | #[test] 194 | fn push_zero() { 195 | let stack = Stack::::new(); 196 | stack.try_push(16).expect("failed to allocate"); 197 | stack.try_push(0).expect("failed to allocate"); 198 | stack.try_push(0).expect("failed to allocate"); 199 | } 200 | 201 | #[test] 202 | fn contains_allocated() { 203 | let stack = Stack::::new(); 204 | let ptr = stack.try_push(1).expect("failed to allocate"); 205 | assert!(stack.contains_allocated(ptr, 1)); 206 | let ptr = stack.try_push(14).expect("failed to allocate"); 207 | assert!(stack.contains_allocated(ptr, 14)); 208 | let ptr = stack.try_push(1).expect("failed to allocate"); 209 | assert!(stack.contains_allocated(ptr, 1)); 210 | let ptr = stack.try_push(0).expect("failed to allocate"); 211 | assert!(stack.contains_allocated(ptr, 0)); 212 | assert!(stack.contains_allocated(core::ptr::null(), 0)); 213 | assert!(!stack.contains_allocated(core::ptr::null(), 1)); 214 | assert!(!stack.contains_allocated(&*Box::new(1), 1)); 215 | } 216 | 217 | #[test] 218 | fn unique() { 219 | let stack = Stack::::new(); 220 | let ptr1 = unsafe { &mut *stack.try_push(1).expect("failed to allocate") }; 221 | *ptr1 = 1; 222 | let ptr2 = unsafe { &mut *stack.try_push(1).expect("failed to allocate") }; 223 | *ptr2 = 2; 224 | assert_eq!(*ptr1, 1); 225 | assert_eq!(*ptr2, 2); 226 | unsafe { 227 | stack.pop_unchecked(1); 228 | } 229 | assert_eq!(*ptr1, 1); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/heterogeneous_stack/heap.rs: -------------------------------------------------------------------------------- 1 | use super::align::assert_aligned; 2 | use alloc::alloc; 3 | use core::alloc::Layout; 4 | use core::marker::PhantomData; 5 | 6 | /// A heap-backed allocator. 7 | /// 8 | /// This allocator can allocate values with sizes that are multiples of `align_of::()`, 9 | /// guaranteeing alignment to `align_of::()`. 10 | pub struct Heap(PhantomData); 11 | 12 | impl Heap { 13 | /// Create an allocator. 14 | pub const fn new() -> Self { 15 | Self(PhantomData) 16 | } 17 | 18 | /// Allocate `n` bytes. 19 | /// 20 | /// The returned pointer is guaranteed to be aligned to `align_of::()` and valid for 21 | /// reads/writes for `n` bytes. It is also guaranteed to be unique. 22 | /// 23 | /// # Panics 24 | /// 25 | /// Panics if `n` is not a multiple of `align_of::()` or `n` is 0, or if out of memory. 26 | #[expect( 27 | clippy::unused_self, 28 | reason = "Using a static method is harder in presence of generic parameters" 29 | )] 30 | pub fn alloc(&self, n: usize) -> *mut u8 { 31 | assert_aligned::(n); 32 | assert_ne!(n, 0, "Allocating 0 bytes is invalid"); 33 | isize::try_from(n.next_multiple_of(align_of::())).expect("Too big allocation"); 34 | // SAFETY: 35 | // - `align` is a power of two, as `align_of` returns a power of two 36 | // - We've checked that `n` fits in `isize` after rounding up 37 | let layout = unsafe { Layout::from_size_align_unchecked(n, align_of::()) }; 38 | // SAFETY: n != 0 has been checked 39 | unsafe { alloc::alloc(layout) } 40 | } 41 | 42 | /// Deallocate `n` bytes. 43 | /// 44 | /// # Safety 45 | /// 46 | /// The caller must ensure that the pointer was produced by a call to [`Heap::alloc`] with the 47 | /// same value of `n`. In addition, references to the deallocated memory must not be used after 48 | /// `dealloc` is called. 49 | #[expect( 50 | clippy::unused_self, 51 | reason = "Using a static method is harder in presence of generic parameters" 52 | )] 53 | pub unsafe fn dealloc(&self, ptr: *mut u8, n: usize) { 54 | // SAFETY: alloc would fail if the preconditions for this weren't established 55 | let layout = unsafe { Layout::from_size_align_unchecked(n, align_of::()) }; 56 | // SAFETY: 57 | // - ptr was allocated with the same allocator 58 | // - alloc would fail if n == 0, so we know n != 0 holds here 59 | unsafe { alloc::dealloc(ptr, layout) } 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use super::*; 66 | 67 | #[test] 68 | #[should_panic] 69 | fn alloc_zero() { 70 | Heap::::new().alloc(0); 71 | } 72 | 73 | #[test] 74 | #[should_panic] 75 | fn alloc_unaligned() { 76 | Heap::::new().alloc(3); 77 | } 78 | 79 | #[test] 80 | #[should_panic] 81 | fn alloc_large() { 82 | Heap::::new().alloc((isize::MAX as usize) + 1); 83 | } 84 | 85 | #[test] 86 | fn overaligned() { 87 | #[repr(align(256))] 88 | struct Overaligned; 89 | let heap = Heap::::new(); 90 | let ptr = heap.alloc(256); 91 | assert_eq!(ptr.addr() % 256, 0); 92 | unsafe { 93 | heap.dealloc(ptr, 256); 94 | } 95 | } 96 | 97 | #[test] 98 | fn unique() { 99 | let heap = Heap::::new(); 100 | let ptr1 = unsafe { &mut *heap.alloc(1) }; 101 | let ptr2 = unsafe { &mut *heap.alloc(1) }; 102 | *ptr1 = 1; 103 | *ptr2 = 2; 104 | assert_eq!(*ptr1, 1); 105 | assert_eq!(*ptr2, 2); 106 | unsafe { 107 | heap.dealloc(ptr1, 1); 108 | } 109 | unsafe { 110 | heap.dealloc(ptr2, 1); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/heterogeneous_stack/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod align; 2 | pub mod array; 3 | pub mod heap; 4 | pub mod unbounded; 5 | -------------------------------------------------------------------------------- /src/heterogeneous_stack/unbounded.rs: -------------------------------------------------------------------------------- 1 | use super::{align::assert_aligned, array::Stack as BoundedStack, heap::Heap}; 2 | 3 | /// A thread-unsafe heterogeneous stack, using statically allocated space when possible. 4 | /// 5 | /// Although the stack doesn't track runtime types, all elements are considered independent. Stack 6 | /// operations must be consistent, i.e. pushing 2 bytes and then popping 1 byte twice is unsound. 7 | // Safety invariants: 8 | // - ZSTs are always allocated on the bounded stack. 9 | pub struct Stack { 10 | bounded_stack: BoundedStack, 11 | heap: Heap, 12 | } 13 | 14 | impl Stack { 15 | /// Create an empty stack. 16 | pub const fn new() -> Self { 17 | Self { 18 | bounded_stack: BoundedStack::new(), 19 | heap: Heap::new(), 20 | } 21 | } 22 | 23 | /// Push an `n`-byte object. 24 | /// 25 | /// The returned pointer is guaranteed to be aligned to `align_of::()` and valid for 26 | /// reads/writes for `n` bytes. It is also guaranteed to be unique. 27 | /// 28 | /// # Panics 29 | /// 30 | /// Panics if `n` is not a multiple of `align_of::()` or if allocating the object 31 | /// fails. 32 | #[inline] 33 | pub fn push(&self, n: usize) -> *mut u8 { 34 | self.bounded_stack 35 | .try_push(n) 36 | .unwrap_or_else(|| self.heap.alloc(n)) 37 | } 38 | 39 | /// Remove an `n`-byte object from the top of the stack. 40 | /// 41 | /// # Safety 42 | /// 43 | /// The caller must ensure that: 44 | /// - The passed pointer was obtained from `push` to this instance of [`Stack`]. 45 | /// - The passed pointer corresponds to the top element of the stack, i.e. it has matching `n`, 46 | /// address, and provenance. 47 | /// - The element is not accessed after the call to `pop`. 48 | pub unsafe fn pop(&self, ptr: *mut u8, n: usize) { 49 | if self.bounded_stack.contains_allocated(ptr, n) { 50 | // SAFETY: 51 | // - `contains_allocated` returned `true`, so either the element is allocated on the 52 | // stack or it's a ZST. ZST allocation always succeeds, so this must be on the stack. 53 | // By the safety requirements, it's the top element of the stack, thus there are at 54 | // least `n` bytes. 55 | // - The element is not accessed after the call by a transitive requirement. 56 | unsafe { 57 | self.bounded_stack.pop_unchecked(n); 58 | } 59 | } else { 60 | // SAFETY: `contains_allocated` returned `false`, so the allocation is not on the stack. 61 | // By the requirements, the pointer was produced by `push`, so the allocation has to be 62 | // on the heap. 63 | unsafe { 64 | self.heap.dealloc(ptr, n); 65 | } 66 | } 67 | } 68 | 69 | /// Modify the last element, possibly changing its size. 70 | /// 71 | /// This is a more efficient version of 72 | /// 73 | /// ```no_compile 74 | /// stack.pop(ptr, old_n); 75 | /// stack.push(new_n) 76 | /// ``` 77 | /// 78 | /// # Panics 79 | /// 80 | /// Panics if `new_n` is not a multiple of `align_of::()` or if allocating the object 81 | /// fails. 82 | /// 83 | /// # Safety 84 | /// 85 | /// The same considerations apply as to [`Stack::pop`]. The caller must ensure that: 86 | /// - The passed pointer was obtained from `push` (or `replace_last`) to this instance of 87 | /// [`Stack`]. 88 | /// - The passed pointer corresponds to the top element of the stack (i.e. has matching `old_n`, 89 | /// address, and provenance). 90 | /// - The element is not accessed after the call to `replace_last`. 91 | #[inline(always)] 92 | pub unsafe fn replace_last(&self, old_ptr: *mut u8, old_n: usize, new_n: usize) -> *mut u8 { 93 | assert_aligned::(new_n); 94 | if old_n == new_n { 95 | // Can reuse the allocation 96 | return old_ptr; 97 | } 98 | let was_on_stack = self.bounded_stack.contains_allocated(old_ptr, old_n); 99 | // SAFETY: Valid by transitive requirements. 100 | unsafe { 101 | self.pop(old_ptr, old_n); 102 | } 103 | if was_on_stack && new_n < old_n { 104 | let new_ptr = self.bounded_stack.try_push(new_n); 105 | // SAFETY: If the previous allocation was on the stack and the new allocation is 106 | // smaller, it must necessarily succeed. 107 | return unsafe { new_ptr.unwrap_unchecked() }; 108 | } 109 | if !was_on_stack && new_n > old_n { 110 | // If the previous allocation was on the heap and the new allocation is bigger, it won't 111 | // fit on stack either. 112 | return self.heap.alloc(new_n); 113 | } 114 | self.push(new_n) 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod test { 120 | use super::*; 121 | 122 | #[test] 123 | #[should_panic] 124 | fn unaligned_push() { 125 | Stack::::new().push(3); 126 | } 127 | 128 | #[test] 129 | fn overaligned() { 130 | #[repr(align(256))] 131 | struct Overaligned; 132 | let stack = Stack::::new(); 133 | let ptr1 = stack.push(256); 134 | assert_eq!(ptr1.addr() % 256, 0); 135 | let ptr2 = stack.push(256 * 20); 136 | assert_eq!(ptr2.addr() % 256, 0); 137 | unsafe { 138 | stack.pop(ptr2, 256 * 20); 139 | } 140 | unsafe { 141 | stack.pop(ptr1, 256); 142 | } 143 | } 144 | 145 | #[test] 146 | fn allocate() { 147 | let stack = Stack::::new(); 148 | stack.push(5); 149 | unsafe { 150 | stack.pop(stack.push(4097), 4097); 151 | } 152 | } 153 | 154 | #[test] 155 | fn simple() { 156 | let stack = Stack::::new(); 157 | let ptr = stack.push(5); 158 | unsafe { 159 | stack.pop(ptr, 5); 160 | } 161 | } 162 | 163 | #[test] 164 | fn push_zero() { 165 | let stack = Stack::::new(); 166 | let ptr1 = stack.push(4096); 167 | let ptr2 = stack.push(0); 168 | let ptr3 = stack.push(1); 169 | unsafe { 170 | stack.pop(ptr3, 1); 171 | } 172 | unsafe { 173 | stack.pop(ptr2, 0); 174 | } 175 | unsafe { 176 | stack.pop(ptr1, 4096); 177 | } 178 | } 179 | 180 | #[test] 181 | fn spill_over() { 182 | let stack = Stack::::new(); 183 | let ptr1 = stack.push(4095); 184 | let ptr2 = stack.push(1); 185 | let ptr3 = stack.push(1); 186 | unsafe { 187 | stack.pop(ptr3, 1); 188 | } 189 | unsafe { 190 | stack.pop(ptr2, 1); 191 | } 192 | unsafe { 193 | stack.pop(ptr1, 4095); 194 | } 195 | } 196 | 197 | #[test] 198 | fn unique() { 199 | let stack = Stack::::new(); 200 | let ptr1 = unsafe { &mut *stack.push(1) }; 201 | *ptr1 = 1; 202 | let ptr2 = unsafe { &mut *stack.push(1) }; 203 | *ptr2 = 2; 204 | assert_eq!(*ptr1, 1); 205 | assert_eq!(*ptr2, 2); 206 | unsafe { 207 | stack.pop(ptr2, 1); 208 | } 209 | assert_eq!(*ptr1, 1); 210 | } 211 | 212 | #[test] 213 | #[should_panic] 214 | fn unaligned_replace_last() { 215 | let stack = Stack::::new(); 216 | let ptr = stack.push(2); 217 | unsafe { 218 | stack.replace_last(ptr, 2, 3); 219 | } 220 | } 221 | 222 | unsafe fn assert_unique(ptr: *mut u8, n: usize) { 223 | let slice = unsafe { core::slice::from_raw_parts_mut(ptr, n) }; 224 | for x in slice { 225 | *x = 1; 226 | } 227 | } 228 | 229 | #[test] 230 | fn replace_last_on_stack() { 231 | let stack = Stack::::new(); 232 | assert_eq!(stack.bounded_stack.len.get(), 0); 233 | let ptr1 = stack.push(2); 234 | unsafe { 235 | assert_unique(ptr1, 2); 236 | } 237 | assert_eq!(stack.bounded_stack.len.get(), 2); 238 | let ptr2 = unsafe { stack.replace_last(ptr1, 2, 2) }; 239 | unsafe { 240 | assert_unique(ptr2, 2); 241 | } 242 | assert_eq!(stack.bounded_stack.len.get(), 2); 243 | assert_eq!(ptr1, ptr2); 244 | let ptr3 = unsafe { stack.replace_last(ptr2, 2, 5) }; 245 | unsafe { 246 | assert_unique(ptr3, 5); 247 | } 248 | assert_eq!(stack.bounded_stack.len.get(), 5); 249 | assert_eq!(ptr2.addr(), ptr3.addr()); 250 | let ptr4 = unsafe { stack.replace_last(ptr3, 5, 3) }; 251 | unsafe { 252 | assert_unique(ptr4, 3); 253 | } 254 | assert_eq!(stack.bounded_stack.len.get(), 3); 255 | assert_eq!(ptr3.addr(), ptr4.addr()); 256 | } 257 | 258 | #[test] 259 | fn replace_last_on_heap() { 260 | let stack = Stack::::new(); 261 | assert_eq!(stack.bounded_stack.len.get(), 0); 262 | let ptr1 = stack.push(4097); 263 | unsafe { 264 | assert_unique(ptr1, 4097); 265 | } 266 | assert_eq!(stack.bounded_stack.len.get(), 0); 267 | let ptr2 = unsafe { stack.replace_last(ptr1, 4097, 4097) }; 268 | unsafe { 269 | assert_unique(ptr2, 4097); 270 | } 271 | assert_eq!(stack.bounded_stack.len.get(), 0); 272 | assert_eq!(ptr1, ptr2); 273 | let ptr3 = unsafe { stack.replace_last(ptr2, 4097, 4098) }; 274 | unsafe { 275 | assert_unique(ptr3, 4098); 276 | } 277 | assert_eq!(stack.bounded_stack.len.get(), 0); 278 | let ptr4 = unsafe { stack.replace_last(ptr3, 4098, 4097) }; 279 | unsafe { 280 | assert_unique(ptr4, 4097); 281 | } 282 | assert_eq!(stack.bounded_stack.len.get(), 0); 283 | unsafe { 284 | stack.pop(ptr4, 4097); 285 | } 286 | } 287 | 288 | #[test] 289 | fn replace_last_relocate() { 290 | let stack = Stack::::new(); 291 | assert_eq!(stack.bounded_stack.len.get(), 0); 292 | let ptr1 = stack.push(4096); 293 | unsafe { 294 | assert_unique(ptr1, 4096); 295 | } 296 | assert_eq!(stack.bounded_stack.len.get(), 4096); 297 | let ptr2 = unsafe { stack.replace_last(ptr1, 4096, 4097) }; 298 | unsafe { 299 | assert_unique(ptr2, 4097); 300 | } 301 | assert_eq!(stack.bounded_stack.len.get(), 0); 302 | assert_ne!(ptr1, ptr2); 303 | let ptr3 = unsafe { stack.replace_last(ptr2, 4097, 4096) }; 304 | unsafe { 305 | assert_unique(ptr3, 4096); 306 | } 307 | assert_eq!(stack.bounded_stack.len.get(), 4096); 308 | assert_eq!(ptr1.addr(), ptr3.addr()); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/intrinsic.rs: -------------------------------------------------------------------------------- 1 | use core::mem::ManuallyDrop; 2 | 3 | union Data { 4 | init: (ManuallyDrop, ManuallyDrop), 5 | ok: ManuallyDrop, 6 | err: ManuallyDrop, 7 | } 8 | 9 | /// Catch unwinding from a function. 10 | /// 11 | /// Runs `func`. If `func` doesn't unwind, wraps its return value in `Ok` and returns. If `func` 12 | /// unwinds, runs `catch` inside the catch handler and wraps its return value in `Err`. If `catch` 13 | /// unwinds, the process aborts. 14 | /// 15 | /// The argument to `catch` is target-dependent and matches the exception object as supplied by 16 | /// [`core::intrinsics::catch_unwind`]. See rustc sources for specifics. 17 | #[allow( 18 | clippy::missing_errors_doc, 19 | reason = "`Err` value is described immediately" 20 | )] 21 | #[inline] 22 | pub fn intercept T, Catch: FnOnce(*mut u8) -> E, T, E>( 23 | func: Func, 24 | catch: Catch, 25 | ) -> Result { 26 | let mut data: Data = Data { 27 | init: (ManuallyDrop::new(func), ManuallyDrop::new(catch)), 28 | }; 29 | 30 | // SAFETY: `do_catch` is marked as `#[rustc_nounwind]` 31 | if unsafe { 32 | core::intrinsics::catch_unwind( 33 | do_call::, 34 | (&raw mut data).cast(), 35 | do_catch::, 36 | ) 37 | } == 0i32 38 | { 39 | // SAFETY: If zero was returned, no unwinding happened, so `do_call` must have finished till 40 | // the assignment to `data.ok`. 41 | Ok(ManuallyDrop::into_inner(unsafe { data.ok })) 42 | } else { 43 | // SAFETY: If a non-zero value was returned, unwinding has happened, so `do_catch` was 44 | // invoked, thus `data.err` is initialized now. 45 | Err(ManuallyDrop::into_inner(unsafe { data.err })) 46 | } 47 | } 48 | 49 | // This function should be unsafe, but isn't due to the definition of `catch_unwind`. 50 | #[inline] 51 | fn do_call R, Catch: FnOnce(*mut u8) -> E, R, E>(data: *mut u8) { 52 | // SAFETY: `data` is provided by the `catch_unwind` intrinsic, which copies the pointer to the 53 | // `data` variable. 54 | let data: &mut Data = unsafe { &mut *data.cast() }; 55 | // SAFETY: This function is called at the start of the process, so the `init.0` field is still 56 | // initialized. 57 | let func = unsafe { ManuallyDrop::take(&mut data.init.0) }; 58 | data.ok = ManuallyDrop::new(func()); 59 | } 60 | 61 | // This function should be unsafe, but isn't due to the definition of `catch_unwind`. 62 | #[inline] 63 | #[rustc_nounwind] 64 | fn do_catch R, Catch: FnOnce(*mut u8) -> E, R, E>(data: *mut u8, ex: *mut u8) { 65 | // SAFETY: `data` is provided by the `catch_unwind` intrinsic, which copies the pointer to the 66 | // `data` variable. 67 | let data: &mut Data = unsafe { &mut *data.cast() }; 68 | // SAFETY: This function is called immediately after `do_call`, so the `init.1` field is still 69 | // initialized. 70 | let catch = unsafe { ManuallyDrop::take(&mut data.init.1) }; 71 | data.err = ManuallyDrop::new(catch(ex)); 72 | } 73 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Lightweight exceptions. 2 | //! 3 | //! Lithium provides a custom exception mechanism as an alternative to Rust panics. Compared to Rust 4 | //! panics, this mechanism is allocation-free, avoids indirections and RTTI, and is hence faster, if 5 | //! less applicable. 6 | //! 7 | //! On nightly, Lithium is more than 2x faster than Rust panics on common `Result`-like usecases. 8 | //! See the [benchmark](https://github.com/iex-rs/lithium/blob/master/benches/bench.rs). 9 | //! 10 | //! 11 | //! # Usage 12 | //! 13 | //! Throw an exception with [`throw`], catch it with [`catch`] or the more low-level [`intercept`]. 14 | //! Unlike with Rust panics, non-[`Send`] and non-`'static` types can be used soundly. 15 | //! 16 | //! Using the `panic = "abort"` strategy breaks Lithium; avoid doing that. 17 | //! 18 | //! For interop, all crates that depend on Lithium need to use the same version: 19 | //! 20 | //! ```toml 21 | //! [dependencies] 22 | //! lithium = "1" 23 | //! ``` 24 | //! 25 | //! If you break either of these two requirements, cargo will scream at you. 26 | //! 27 | //! 28 | //! # Platform support 29 | //! 30 | //! On stable Rust, Lithium uses the built-in panic mechanism, tweaking it to increase performance 31 | //! just a little bit. 32 | //! 33 | //! On nightly Rust, Lithium uses a custom mechanism on the following targets: 34 | //! 35 | //! |Target |Implementation |Performance | 36 | //! |-------------------|---------------|---------------------------------------------| 37 | //! |Linux, macOS |Itanium EH ABI |2.5x faster than panics | 38 | //! |Windows (MSVC ABI) |SEH |1.5x faster than panics | 39 | //! |Windows (GNU ABI) |Itanium EH ABI |2.5x faster than panics, but slower than MSVC| 40 | //! |Emscripten (old EH)|C++ exceptions |2x faster than panics | 41 | //! |Emscripten (new EH)|Wasm exceptions|2.5x faster than panics | 42 | //! |WASI |Itanium EH ABI |2.5x faster than panics | 43 | //! 44 | //! Lithium strives to support all targets that Rust panics support. If Lithium does not work 45 | //! correctly on such a target, please [open an issue](https://github.com/iex-rs/lithium/issues/). 46 | //! 47 | //! On nightly, Lithium can work without `std` on certain platforms that expose native thread 48 | //! locals and link in an Itanium-style unwinder. Such situations are best handled on a case-by-case 49 | //! basis: [open an issue](https://github.com/iex-rs/lithium/issues/) if you would like to see 50 | //! support for a certain `std`-less target. 51 | //! 52 | //! 53 | //! # Safety 54 | //! 55 | //! Exceptions lack dynamic typing information. For soundness, the thrown and caught types must 56 | //! match exactly. Note that the functions are generic, and if the type is inferred wrong, UB will 57 | //! happen. Use turbofish to avoid this pitfall. 58 | //! 59 | //! The matching types requirement only applies to exceptions that aren't caught inside the 60 | //! [`catch`]/[`intercept`] callback. For example, this is sound: 61 | //! 62 | //! ```rust 63 | //! use lithium::{catch, throw}; 64 | //! 65 | //! struct A; 66 | //! struct B; 67 | //! 68 | //! unsafe { 69 | //! let _ = catch::<_, A>(|| { 70 | //! let _ = catch::<_, B>(|| throw(B)); 71 | //! throw(A); 72 | //! }); 73 | //! } 74 | //! ``` 75 | //! 76 | //! The responsibility of upholding this safety requirement is split between the throwing and the 77 | //! catching functions. All throwing functions must be `unsafe`, listing "only caught by type `E`" 78 | //! as a safety requirement. All catching functions that take a user-supplied callback must be 79 | //! `unsafe` too, listing "callback only throws type `E`" as a safety requirement. 80 | //! 81 | //! Although seemingly redundant, this enables safe abstractions over exceptions when both the 82 | //! throwing and the catching functions are provided by one crate. As long as the exception types 83 | //! used by the crate match, all safe user-supplied callbacks are sound to call, because safe 84 | //! callbacks can only interact with exceptions in an isolated manner. 85 | 86 | #![no_std] 87 | #![cfg_attr(all(thread_local = "attribute"), feature(thread_local))] 88 | #![cfg_attr( 89 | any(backend = "itanium", backend = "seh", backend = "emscripten"), 90 | expect( 91 | internal_features, 92 | reason = "Can't do anything about core::intrinsics::catch_unwind yet", 93 | ) 94 | )] 95 | #![cfg_attr( 96 | any(backend = "itanium", backend = "seh", backend = "emscripten"), 97 | feature(core_intrinsics, rustc_attrs) 98 | )] 99 | #![cfg_attr(backend = "seh", feature(fn_ptr_trait, std_internals))] 100 | #![cfg_attr( 101 | all(backend = "itanium", target_arch = "wasm32"), 102 | feature(wasm_exception_handling_intrinsics) 103 | )] 104 | #![deny(unsafe_op_in_unsafe_fn)] 105 | #![warn( 106 | clippy::cargo, 107 | clippy::pedantic, 108 | clippy::alloc_instead_of_core, 109 | clippy::allow_attributes_without_reason, 110 | clippy::arithmetic_side_effects, 111 | clippy::as_underscore, 112 | clippy::assertions_on_result_states, 113 | clippy::clone_on_ref_ptr, 114 | clippy::decimal_literal_representation, 115 | clippy::default_numeric_fallback, 116 | clippy::deref_by_slicing, 117 | clippy::else_if_without_else, 118 | clippy::empty_drop, 119 | clippy::empty_enum_variants_with_brackets, 120 | clippy::empty_structs_with_brackets, 121 | clippy::exhaustive_enums, 122 | clippy::exhaustive_structs, 123 | clippy::fn_to_numeric_cast_any, 124 | clippy::format_push_string, 125 | clippy::infinite_loop, 126 | clippy::inline_asm_x86_att_syntax, 127 | clippy::mem_forget, // use ManuallyDrop instead 128 | clippy::missing_assert_message, 129 | clippy::missing_const_for_fn, 130 | clippy::missing_inline_in_public_items, 131 | clippy::mixed_read_write_in_expression, 132 | clippy::multiple_unsafe_ops_per_block, 133 | clippy::mutex_atomic, 134 | clippy::needless_raw_strings, 135 | clippy::pub_without_shorthand, 136 | clippy::rc_buffer, 137 | clippy::rc_mutex, 138 | clippy::redundant_type_annotations, 139 | clippy::rest_pat_in_fully_bound_structs, 140 | clippy::same_name_method, 141 | clippy::self_named_module_files, 142 | clippy::semicolon_inside_block, 143 | clippy::separated_literal_suffix, 144 | clippy::shadow_unrelated, 145 | clippy::std_instead_of_alloc, 146 | clippy::std_instead_of_core, 147 | clippy::string_lit_chars_any, 148 | clippy::string_to_string, 149 | clippy::tests_outside_test_module, 150 | clippy::try_err, 151 | clippy::undocumented_unsafe_blocks, 152 | clippy::unnecessary_safety_comment, 153 | clippy::unnecessary_safety_doc, 154 | clippy::unnecessary_self_imports, 155 | clippy::unneeded_field_pattern, 156 | clippy::unused_result_ok, 157 | clippy::wildcard_enum_match_arm, 158 | )] 159 | #![allow( 160 | clippy::inline_always, 161 | reason = "I'm not an idiot, this is a result of benchmarking/profiling" 162 | )] 163 | 164 | #[cfg(panic = "abort")] 165 | compile_error!("Using Lithium with panic = \"abort\" is unsupported"); 166 | 167 | #[cfg(any(abort = "std", backend = "panic", thread_local = "std", test))] 168 | extern crate std; 169 | 170 | extern crate alloc; 171 | 172 | mod api; 173 | mod backend; 174 | 175 | #[cfg(any(backend = "itanium", backend = "emscripten", backend = "panic"))] 176 | mod heterogeneous_stack; 177 | #[cfg(any(backend = "itanium", backend = "emscripten", backend = "panic"))] 178 | mod stacked_exceptions; 179 | 180 | #[cfg(any(backend = "itanium", backend = "seh", backend = "emscripten"))] 181 | mod intrinsic; 182 | 183 | pub use api::{catch, intercept, throw, InFlightException}; 184 | 185 | /// Abort the process with a message. 186 | /// 187 | /// If `std` is available, this also outputs a message to stderr before aborting. 188 | #[cfg(any(backend = "itanium", backend = "seh", backend = "emscripten"))] 189 | #[cold] 190 | #[inline(never)] 191 | fn abort(message: &str) -> ! { 192 | #[cfg(abort = "std")] 193 | { 194 | use std::io::Write; 195 | let _ = std::io::stderr().write_all(message.as_bytes()); 196 | std::process::abort(); 197 | } 198 | 199 | // This is a nightly-only method, but all three backends this is enabled under require nightly 200 | // anyway, so this is no big deal. 201 | #[cfg(not(abort = "std"))] 202 | { 203 | let _ = message; 204 | core::intrinsics::abort(); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/stacked_exceptions.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | backend::{ActiveBackend, RethrowHandle, ThrowByPointer, ThrowByValue}, 3 | heterogeneous_stack::unbounded::Stack, 4 | }; 5 | use core::mem::{offset_of, ManuallyDrop}; 6 | 7 | // SAFETY: 8 | // - The main details are forwarded to the `ThrowByPointer` impl. 9 | // - We ask the impl not to modify the object, just the header, so the object stays untouched. 10 | unsafe impl ThrowByValue for ActiveBackend { 11 | type RethrowHandle = PointerRethrowHandle; 12 | 13 | #[inline] 14 | unsafe fn throw(cause: E) -> ! { 15 | let ex = push(cause); 16 | // SAFETY: Just allocated. 17 | let ex = unsafe { Exception::header(ex) }; 18 | // SAFETY: 19 | // - The exception is a unique pointer to an exception object, as allocated by `push`. 20 | // - "Don't mess with exceptions" is required transitively. 21 | unsafe { 22 | ::throw(ex); 23 | } 24 | } 25 | 26 | #[inline] 27 | unsafe fn intercept R, R, E>( 28 | func: Func, 29 | ) -> Result)> { 30 | ::intercept(func).map_err(|ex| { 31 | // SAFETY: By the safety requirement, unwinding could only happen from `throw` with type 32 | // `E`. Backend guarantees the pointer is passed as-is, and `throw` only throws unique 33 | // pointers to valid instances of `Exception` via the backend. 34 | let ex = unsafe { Exception::::from_header(ex) }; 35 | let cause = { 36 | // SAFETY: Same as above. 37 | let ex_ref = unsafe { &mut *ex }; 38 | // SAFETY: We only read the cause here once. 39 | unsafe { ex_ref.cause() } 40 | }; 41 | (cause, PointerRethrowHandle { ex }) 42 | }) 43 | } 44 | } 45 | 46 | // Type invariant: `ex` is a unique pointer to the exception object on the exception stack. 47 | #[derive(Debug)] 48 | pub(crate) struct PointerRethrowHandle { 49 | ex: *mut Exception, 50 | } 51 | 52 | impl Drop for PointerRethrowHandle { 53 | #[inline] 54 | fn drop(&mut self) { 55 | // SAFETY: 56 | // - `ex` is a unique pointer to the exception object by the type invariant. 57 | // - The safety requirement on `intercept` requires that all exceptions that are thrown 58 | // between `intercept` and `drop` are balanced. This exception was at the top of the stack 59 | // when `intercept` returned, so it must still be at the top when `drop` is invoked. 60 | unsafe { pop(self.ex) } 61 | } 62 | } 63 | 64 | impl RethrowHandle for PointerRethrowHandle { 65 | #[inline] 66 | unsafe fn rethrow(self, new_cause: F) -> ! { 67 | let ex = core::mem::ManuallyDrop::new(self); 68 | // SAFETY: The same logic that proves `pop` in `drop` is valid applies here. We're not 69 | // *really* dropping `self`, but the user code does not know that. 70 | let ex = unsafe { replace_last(ex.ex, new_cause) }; 71 | // SAFETY: Just allocated. 72 | let ex = unsafe { Exception::header(ex) }; 73 | // SAFETY: 74 | // - `ex` is a unique pointer to the exception object because it was just produced by 75 | // `replace_last`. 76 | // - "Don't mess with exceptions" is required transitively. 77 | unsafe { 78 | ::throw(ex); 79 | } 80 | } 81 | } 82 | 83 | type Header = ::ExceptionHeader; 84 | 85 | /// An exception object, to be used by the backend. 86 | pub struct Exception { 87 | header: Header, 88 | cause: ManuallyDrop>, 89 | } 90 | 91 | #[repr(C, packed)] 92 | struct Unaligned(T); 93 | 94 | impl Exception { 95 | /// Create a new exception to be thrown. 96 | fn new(cause: E) -> Self { 97 | Self { 98 | header: ActiveBackend::new_header(), 99 | cause: ManuallyDrop::new(Unaligned(cause)), 100 | } 101 | } 102 | 103 | /// Get pointer to header. 104 | /// 105 | /// # Safety 106 | /// 107 | /// `ex` must be a unique pointer at an exception object. 108 | pub const unsafe fn header(ex: *mut Self) -> *mut Header { 109 | // SAFETY: Required transitively. 110 | unsafe { ex.byte_add(offset_of!(Self, header)) }.cast() 111 | } 112 | 113 | /// Restore pointer from pointer to header. 114 | /// 115 | /// # Safety 116 | /// 117 | /// `header` must have been produced by [`Exception::header`], and the corresponding object must 118 | /// be alive. 119 | pub const unsafe fn from_header(header: *mut Header) -> *mut Self { 120 | // SAFETY: Required transitively. 121 | unsafe { header.byte_sub(offset_of!(Self, header)) }.cast() 122 | } 123 | 124 | /// Get the cause of the exception. 125 | /// 126 | /// # Safety 127 | /// 128 | /// This function returns a bitwise copy of the cause. This means that it can only be called 129 | /// once on each exception. 130 | pub unsafe fn cause(&mut self) -> E { 131 | // SAFETY: We transitively require that the cause is not read twice. 132 | unsafe { ManuallyDrop::take(&mut self.cause).0 } 133 | } 134 | } 135 | 136 | #[cfg(thread_local = "std")] 137 | std::thread_local! { 138 | /// Thread-local exception stack. 139 | static STACK: Stack

= const { Stack::new() }; 140 | } 141 | 142 | #[cfg(thread_local = "attribute")] 143 | #[thread_local] 144 | static STACK: Stack
= const { Stack::new() }; 145 | 146 | /// Get a reference to the thread-local exception stack. 147 | /// 148 | /// # Safety 149 | /// 150 | /// The reference is lifetime-extended to `'static` and is only valid for access until the end of 151 | /// the thread. This includes at least the call frame of the immediate caller. 152 | // Unfortunately, replacing this unsafe API with a safe `with_stack` doesn't work, as `with` fails 153 | // to inline. 154 | #[inline] 155 | unsafe fn get_stack() -> &'static Stack
{ 156 | #[cfg(thread_local = "std")] 157 | // SAFETY: We require the caller to not use the reference anywhere near the end of the thread, 158 | // so as long as `with` succeeds, there is no problem. 159 | return STACK.with(|r| unsafe { core::mem::transmute(r) }); 160 | 161 | #[cfg(thread_local = "attribute")] 162 | // SAFETY: We require the caller to not use the reference anywhere near the end of the thread, 163 | // so if `&STACK` is sound in the first place, there is no problem. 164 | return unsafe { core::mem::transmute::<&Stack
, &'static Stack
>(&STACK) }; 165 | 166 | #[cfg(thread_local = "unimplemented")] 167 | compile_error!("Unable to compile Lithium on a platform does not support thread locals") 168 | } 169 | 170 | const fn get_alloc_size() -> usize { 171 | const { 172 | assert!( 173 | align_of::>() == align_of::
(), 174 | "Exception has unexpected alignment", 175 | ); 176 | } 177 | // This is a multiple of align_of::>(), which we've just checked to be equal to the 178 | // alignment used for the stack. 179 | size_of::>() 180 | } 181 | 182 | /// Push an exception onto the thread-local exception stack. 183 | #[inline(always)] 184 | pub fn push(cause: E) -> *mut Exception { 185 | // SAFETY: We don't let the stack leak past the call frame. 186 | let stack = unsafe { get_stack() }; 187 | let ex: *mut Exception = stack.push(get_alloc_size::()).cast(); 188 | // SAFETY: 189 | // - The stack allocator guarantees the pointer is dereferenceable and unique. 190 | // - The stack is configured to align like Header, which get_alloc_size verifies to be the 191 | // alignment of Exception. 192 | unsafe { 193 | ex.write(Exception::new(cause)); 194 | } 195 | ex 196 | } 197 | 198 | /// Remove an exception from the thread-local exception stack. 199 | /// 200 | /// # Safety 201 | /// 202 | /// The caller must ensure `ex` corresponds to the exception at the top of the stack, as returned by 203 | /// [`push`] or [`replace_last`] with the same exception type. In addition, the exception must not 204 | /// be accessed after `pop`. 205 | pub unsafe fn pop(ex: *mut Exception) { 206 | // SAFETY: We don't let the stack leak past the call frame. 207 | let stack = unsafe { get_stack() }; 208 | // SAFETY: We require `ex` to be correctly obtained and unused after `pop`. 209 | unsafe { 210 | stack.pop(ex.cast(), get_alloc_size::()); 211 | } 212 | } 213 | 214 | /// Replace the exception on the top of the thread-local exception stack. 215 | /// 216 | /// # Safety 217 | /// 218 | /// The caller must ensure `ex` corresponds to the exception at the top of the stack, as returned by 219 | /// [`push`] or [`replace_last`] with the same exception type. In addition, the old exception must 220 | /// not be accessed after `replace_last`. 221 | pub unsafe fn replace_last(ex: *mut Exception, cause: F) -> *mut Exception { 222 | // SAFETY: We don't let the stack leak past the call frame. 223 | let stack = unsafe { get_stack() }; 224 | let ex: *mut Exception = 225 | // SAFETY: We require `ex` to be correctly obtained and unused after `replace_last`. 226 | unsafe { stack.replace_last(ex.cast(), get_alloc_size::(), get_alloc_size::()) } 227 | .cast(); 228 | // SAFETY: `replace_last` returns unique aligned storage, good for Exception as per the 229 | // return value of `get_alloc_size`. 230 | unsafe { 231 | ex.write(Exception::new(cause)); 232 | } 233 | ex 234 | } 235 | 236 | #[cfg(test)] 237 | mod test { 238 | use super::*; 239 | use alloc::string::String; 240 | 241 | #[test] 242 | fn exception_cause() { 243 | let mut ex = Exception::new(String::from("Hello, world!")); 244 | assert_eq!(unsafe { ex.cause() }, "Hello, world!"); 245 | } 246 | 247 | #[test] 248 | fn stack() { 249 | let ex1 = push(String::from("Hello, world!")); 250 | let ex2 = push(123i32); 251 | assert_eq!(unsafe { (*ex2).cause() }, 123); 252 | let ex3 = unsafe { replace_last(ex2, "Third time's a charm") }; 253 | assert_eq!(unsafe { (*ex3).cause() }, "Third time's a charm"); 254 | unsafe { 255 | pop(ex3); 256 | } 257 | assert_eq!(unsafe { (*ex1).cause() }, "Hello, world!"); 258 | unsafe { 259 | pop(ex1); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /valgrind.supp: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Leak 4 | match-leak-kinds: possible 5 | fun:malloc 6 | ... 7 | fun:_ZN3std6thread6Thread3new* 8 | fun:_ZN3std6thread7current12init_current* 9 | } 10 | --------------------------------------------------------------------------------