├── .github ├── FUNDING.yml └── workflows │ ├── release-plz.yml │ └── test.yml ├── .gitignore ├── Justfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── rubicon ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── build.rs └── src │ └── lib.rs ├── test-crates ├── exports │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mod_a │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mod_b │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mokio │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── samplebin │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── main.rs └── tests ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── package-lock.json └── src └── main.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fasterthanlime] 2 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release-plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust toolchain 22 | uses: dtolnay/rust-toolchain@stable 23 | - name: Run release-plz 24 | uses: MarcoIeni/release-plz-action@v0.5 25 | with: 26 | manifest_path: rubicon/Cargo.toml 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | merge_group: 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Install Rust 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: stable 24 | profile: minimal 25 | override: true 26 | - name: Add nightly 27 | run: rustup toolchain add nightly 28 | - name: Run unit tests 29 | run: | 30 | cargo test --manifest-path rubicon/Cargo.toml 31 | - name: Run tests runner 32 | run: | 33 | cd tests/ 34 | cargo run 35 | shell: bash 36 | continue-on-error: ${{ matrix.os == 'windows-latest' }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # Just manual: https://github.com/casey/just 2 | 3 | check: 4 | cargo hack --each-feature --exclude-all-features clippy --manifest-path rubicon/Cargo.toml 5 | 6 | test *args: 7 | #!/usr/bin/env bash -eux 8 | BIN_CHANNEL="${BIN_CHANNEL:-stable}" 9 | BIN_FLAGS="${BIN_FLAGS:-}" 10 | 11 | SOPRINTLN=1 cargo "+${BIN_CHANNEL}" build --manifest-path test-crates/samplebin/Cargo.toml ${BIN_FLAGS} 12 | 13 | export DYLD_LIBRARY_PATH=$(rustc "+stable" --print sysroot)/lib 14 | export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$(rustc "+nightly" --print sysroot)/lib 15 | export LD_LIBRARY_PATH=$(rustc "+stable" --print sysroot)/lib 16 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(rustc "+nightly" --print sysroot)/lib 17 | 18 | ./test-crates/samplebin/target/debug/samplebin {{args}} 19 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/LICENSE-2.0 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) 2 | [![crates.io](https://img.shields.io/crates/v/rubicon.svg)](https://crates.io/crates/rubicon) 3 | [![docs.rs](https://docs.rs/rubicon/badge.svg)](https://docs.rs/rubicon) 4 | [![cursed? yes](https://img.shields.io/badge/cursed%3F-yes-red.svg)](https://github.com/bearcove/rubicon) 5 | 6 | # rubicon 7 | 8 | ![The rubicon logo: a shallow river in northeastern Italy famously crossed by Julius Caesar in 49 BC](https://github.com/user-attachments/assets/7e10888d-9f44-4395-a2ad-3e3fc0801996) 9 | 10 | _Logo by [MisiasArt](https://misiasart.com)_ 11 | 12 | rubicon enables a form of dynamic linking in Rust through cdylib crates 13 | and carefully-enforced invariants. 14 | 15 | ## Name 16 | 17 | Webster's Dictionary defines 'rubicon' as: 18 | 19 | > a bounding or limiting line. especially: one that 20 | > when crossed, commits a person irrevocably. 21 | 22 | In this case, I see it as the limiting line between several shared objects, 23 | within the same address space, each including their own copy of the same Rust 24 | code. 25 | 26 | ## Nomenclature 27 | 28 | Dynamic linking concepts have different names on different platforms: 29 | 30 | | Concept | Linux | macOS | Windows | 31 | |-------------------- | ------------------- | ------------------------ | -------------------------------------------------------------------- | 32 | | Shared library | shared object | dynamic library | DLL (Dynamic Link Library) | 33 | | Library file name | `libfoo.so` | `libfoo.dylib` | `foo.dll` | 34 | | Library search path | `LD_LIBRARY_PATH` | `DYLD_LIBRARY_PATH` | `PATH` | 35 | | Preload mechanism | `LD_PRELOAD` | `DYLD_INSERT_LIBRARIES` | [It's complicated](https://stackoverflow.com/a/5273439) | 36 | 37 | Throughout this document, macOS naming conventions are preferred. 38 | 39 | ## Motivation 40 | 41 | ### Rust's dynamic linking model (`1graph`) 42 | 43 | (This section is up-to-date as of Rust 1.79 / 2024-07-18) 44 | 45 | cargo and rustc support some form of dynamic linking, through the 46 | [-C prefer-dynamic][prefer-dynamic] compiler flag. 47 | 48 | [prefer-dynamic]: https://doc.rust-lang.org/rustc/codegen-options/index.html#prefer-dynamic 49 | 50 | This flag will: 51 | 52 | * Link against the pre-built `libstd-HASH.dylib`, shipped via rustup 53 | (assuming you're not using `-Z build-std`) 54 | * Try to link against `libfoobar.dylib`, for any crate `foobar` that 55 | includes `dylib` in its `crate-type` 56 | 57 | rustc has an [internal algorithm][] to decide which linkage to use for which 58 | dependency. That algorithm is best-effort, and it can fail. 59 | 60 | [internal algorithm]: https://github.com/rust-lang/rust/blob/master/compiler/rustc_metadata/src/dependency_format.rs 61 | 62 | Regardless, it assumes that rustc has knowledge of the entire dependency graph 63 | at link time. 64 | 65 | ### rubicon's dynamic linking model (`xgraph`) 66 | 67 | However, one might want to split the dependency graph on purpose: 68 | 69 | | Strategy | 1graph (one dependency graph) | xgraph (multiple dependency graphs) | 70 | | ---------------------------- | -------------------------------------------- | ------------------------------------------------------------------- | 71 | | Module crate-type | dylib | cdylib | 72 | | Duplicates in address space | No (rlib/dylib resolution at link time) | Yes (by design) | 73 | | Who loads modules? | the runtime linker | the app | 74 | | When loads modules? | before main, unconditionally | any time (but don't unload) | 75 | | How loads modules? | `DT_NEEDED` / `LC_LOAD_DYLIB` etc. | libdl, likely via [libloading](https://docs.rs/libloading/latest/libloading/) | 76 | 77 | Let's call Rust's "supported" dynamic linking model "1graph". 78 | 79 | rubicon enables (at your own risk), a different model, which we'll call "xgraph". 80 | 81 | In the "xgraph" model, every "module" of your application — anything that might make 82 | sense to build separately, like "a bunch of tree-sitter grammars", or "a whole JavaScript runtime", 83 | is its _own_ dependency graph, rooted at a crate with a `crate-type` of `cdylib`. 84 | 85 | In the "xgraph" model, your application's "shared object" (Linux executables, macOS executables, 86 | etc. are just shared objects — not too different from libraries, except they have an entry point) 87 | does not have any references to its modules — by the time `main()` is executed, none of the 88 | modules are loaded yet. 89 | 90 | Instead, modules are loaded explicitly through a crate like [libloading](https://lib.rs/crates/libloading), 91 | which under the hood, uses whatever facilities the platform's dynamic linker-loader exposes. This 92 | lets you choose which modules to load and when. 93 | 94 | ### Linkage and discipline 95 | 96 | The "xgraph" model is dangerous — we must use discipline to get it to work at all. 97 | 98 | In particular, we'll maintain the following invariants: 99 | 100 | * A. Modules are NEVER UNLOADED, only loaded. 101 | * B. The EXACT SAME RUSTC VERSION is used to build the app and all modules 102 | * C. The EXACT SAME CARGO FEATURES are enabled for crates that both the app 103 | and some modules depend on. 104 | 105 | Unloading modules ("A") would break a significant assumption in all Rust programs: that `'static` 106 | lasts for the entirety of the program's execution. When unloading a module, we can make something 107 | `'static` disappear. 108 | 109 | Although nobody can stop you from unloading modules, what you're writing at this point is no longer 110 | safe Rust. 111 | 112 | Mixing rustc versions ("B") might result in differences in struct layouts, for example. For a struct like: 113 | 114 | ```rust 115 | struct Blah { 116 | a: u64, 117 | b: u32, 118 | } 119 | ``` 120 | 121 | ...there's no guarantee which field will be first, if there will be padding, what order the fields will 122 | be in. We pray that struct layouts match across the same compiler version, but even that might not be 123 | guaranteed? (citation needed) 124 | 125 | Mixing cargo feature sets ("C") might, again, result in differences in struct layouts: 126 | 127 | ```rust 128 | struct Blah { 129 | #[cfg(feature = "foo")] 130 | a: u64, 131 | b: u32 132 | } 133 | 134 | // if the app has `foo` enabled, and we pass a &Blah` to 135 | // a module that doesn't have `foo` enabled, then the 136 | // layout won't match. 137 | ``` 138 | 139 | Or function signatures. Or the (duplicate) code being run at any time. 140 | 141 | ### Duplicates are unavoidable in `xgraph` 142 | 143 | In the `1graph` model, rustc is able to see the entire dependency graph — as a 144 | result, it's able to avoid duplicates of a dependency altogether: if the app 145 | and some of its modules depend on `tokio`, then there'll be a single 146 | `libtokio.dylib` that they all depend on — no duplication whatsoever. 147 | 148 | In the `xgraph` model, we're unable to achieve that. By design, the app and all 149 | of its modules are built and linked in complete isolation. As long as they agree 150 | on a thin FFI (Foreign Function Interface) boundary, which might be provided by 151 | a "common" crate everyone depends on, they can be built. 152 | 153 | It is possible for the app and its modules to link dynamically against `tokio`: 154 | there will be, for each target (the app is a target, each module is a target), 155 | a `libtokio.dylib` file. 156 | 157 | However, that file will not have the same contents for each target, because `tokio` 158 | exposes generic functions. 159 | 160 | This code: 161 | 162 | ```rust 163 | tokio::spawn(async move { 164 | println!("Hello, world!"); 165 | }); 166 | ``` 167 | 168 | Will cause the `spawn` function to be monomorphized, turning from this: 169 | 170 | ```rust 171 | pub fn spawn(future: F) -> JoinHandle ⓘ 172 | where 173 | F: Future + Send + 'static, 174 | F::Output: Send + 'static, 175 | ``` 176 | 177 | Into something like this (the mangling here is not realistic): 178 | 179 | ```rust 180 | pub fn spawn__OpaqueType__FOO(future: OpaqueType__FOO) -> JoinHandle<()> ⓘ 181 | ``` 182 | 183 | If in another module, we have that code: 184 | 185 | ```rust 186 | let jh = tokio::spawn(async move { 187 | // make yourself wanted 188 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 189 | println!("Oh hey, you're early!"); 190 | 42 191 | }); 192 | let answer = jh.await.unwrap(); 193 | ``` 194 | 195 | Then it will cause _another_ monomorphization of `tokio`'s `spawn` function, 196 | which might look something like this: 197 | 198 | ```rust 199 | pub fn spawn__OpaqueType__BAR(future: OpaqueType__BAR) -> JoinHandle ⓘ 200 | ``` 201 | 202 | And now, you'll have: 203 | 204 | ``` 205 | bin/ 206 | app/ 207 | executable 208 | libtokio.dylib 209 | (exports spawn__OpaqueType__FOO) 210 | mod_a/ 211 | libmod_a.dylib 212 | libtokio.dylib 213 | (export spawn__OpaqueType__BAR) 214 | ``` 215 | 216 | At this point, `executable` refers to its own `libtokio.dylib` (by absolute path), 217 | and `libmod_a.dylib`, to its own, separate, `libtokio.dylib`. 218 | 219 | Even if you were to edit the `DT_NEEDED` / `LC_LOAD_DYLIB` information to have the 220 | modules point to `executable`'s version of the dynamic libraries, you would find 221 | yourself with a "missing symbol" error at runtime! 222 | 223 | | libtokio.dylib from | Has __FOO | Has __BAR | 224 | |---------------------|-----------|-----------| 225 | | executable | ✅ | ❌ | 226 | | mod_a | ❌ | ✅ | 227 | 228 | None of the `libtokio.dylib` files you have contain all the symbols required. 229 | 230 | To make a `libtokio.dylib` file that contains ALL THE SYMBOLS required, you 231 | would need rustc to be aware of the whole dependency graph: hence, you'd be back 232 | to the `1graph` model. 233 | 234 | Hence, when using the `xgraph`, we accept the reality that code from dependencies 235 | _will_ be duplicated. 236 | 237 | | target | non-generic code | app generics | mod_a generics | mod_b generics | 238 | |--------|------------------|--------------|----------------|----------------| 239 | | app | ✅ | ✅ | ❌ | ❌ | 240 | | mod_a | ✅ | ❌ | ✅ | ❌ | 241 | | mod_b | ✅ | ❌ | ❌ | ✅ | 242 | 243 | That first column corresponds to all functions, types, etc. that are not generic, 244 | or that are instantiated the exact same way in each independent depgraph. 245 | 246 | There will be a copy of each of these in the application executable AND in each 247 | `libmod_etc.dylib` file. That's unavoidable for now. 248 | 249 | ### Duplicating globals is never okay 250 | 251 | Now that we've made our peace with the fact there _will_ be code duplication, and 252 | that, as long as that code EXACTLY MATCHES across different copies, it's okay, 253 | we need to address the fact that duplicating globals is _never okay_. 254 | 255 | In particular, by globals, we mean: 256 | 257 | * thread-locals (declared via the [std::thread_local!][] macro) 258 | * process-locals (more commonly called "statics", declared via the [static keyword][]) 259 | 260 | ```rust 261 | static sample_process_local: AtomicU64 = AtomicU64::new(0); 262 | 263 | std::thread_local! { 264 | static sample_thread_local: u64 = 42; 265 | } 266 | 267 | fn blah() { 268 | let sample_local = 42; 269 | } 270 | ``` 271 | 272 | | kind | process-local | thread-local | local | 273 | |----------------------|---------------|--------------|--------| 274 | | unique per scope | ❌ | ❌ | ✅ | 275 | | unique per thread | ❌ | ✅ | ✅ | 276 | | unique per process | ✅ | ✅ | ✅ | 277 | 278 | [std::thread_local!]: https://doc.rust-lang.org/std/macro.thread_local.html 279 | [static keyword]: https://doc.rust-lang.org/reference/items/static-items.html 280 | 281 | Take `tracing`, for example: it lets you emit "events" that a "subscriber" can process. 282 | It's used for structured logging: the event could be of level INFO and include information 283 | about some HTTP request, for example. 284 | 285 | `tracing` allows registering a "global" dispatcher, through [tracing::dispatcher::set_global_default][]. 286 | This sets a process-global: 287 | 288 | [tracing::dispatcher::set_global_default]: https://docs.rs/tracing/latest/tracing/dispatcher/fn.set_global_default.html 289 | 290 | ```rust 291 | static mut GLOBAL_DISPATCH: Dispatch = Dispatch { 292 | subscriber: Kind::Global(&NO_SUBSCRIBER), 293 | }; 294 | ``` 295 | 296 | The problem is that, since all targets (the app, all its modules) have their own 297 | copy of `tracing`, they also have their own `GLOBAL_DISPATCH` process-local. 298 | 299 | It doesn't matter to `mod_a` if we've registered a global dispatcher from the app: 300 | according to `mod_a`'s copy of `GLOBAL_DISPATH` — there's no subscriber! 301 | 302 | There's only one fix for this: everyone must share the same `GLOBAL_DISPATCH`: 303 | it must be exported from `app`, and imported from all its modules. 304 | 305 | ## How Rust exports and imports dynamic symbols 306 | 307 | In a perfect world, there'd be a rustc flag like `-C globals-linkage=[import,export]`: 308 | we'd set it to `export` for our app, so that it would declare those as exported symbols, 309 | the kind you can look up with [dlsym][], and that dynamic libraries you load later can 310 | use, because they're part of the set of symbols the dynamic linker-loader searches. 311 | 312 | [dlsym]: https://man7.org/linux/man-pages/man3/dlsym.3.html 313 | 314 | There are, however, two roadblocks we must hop. 315 | 316 | The first is that dynamic symbols are not exported for executables. Luckily, there's 317 | a linker flag for that: `-rdynamic` (also known as `--export-dynamic`). 318 | 319 | The second is that _there is no such rustc flag at all_. 320 | 321 | Export a static is easy enough. Instead of: 322 | 323 | ```rust 324 | static MERCHANDISE: u64 = 42; 325 | ``` 326 | 327 | We can do: 328 | 329 | ```rust 330 | #[used] 331 | static MERCHANDISE: u64 = 42; 332 | ``` 333 | 334 | And we'll get a mangled symbol: 335 | 336 | ```shell 337 | ❯ cargo build --quiet 338 | ❯ nm -gp ./target/debug/librubicon.dylib | grep MERCHANDISE 339 | 00000000000099f0 S __ZN7rubicon11MERCHANDISE17h03e39e78778de1fdE 340 | ``` 341 | 342 | The `#[no_mangle]` attribute implies `#[used]`, and also 343 | disables name mangling: 344 | 345 | ```rust 346 | #[no_mangle] 347 | static MERCHANDISE: u64 = 42; 348 | ``` 349 | 350 | ```shell 351 | ❯ cargo build --quiet 352 | ❯ nm -gp ./target/debug/librubicon.dylib | grep MERCHANDISE 353 | 00000000000099f0 S _MERCHANDISE 354 | ``` 355 | 356 | (Just ignore the `_` prefix — linkers are cute like that.) 357 | 358 | In fact, we can even specify our own export name if we want: 359 | 360 | ```rust 361 | #[export_name = "STILL_MERCHANDISE"] 362 | static PINK_UNICORN: u64 = 42; 363 | ``` 364 | 365 | ```shell 366 | ❯ cargo build --quiet 367 | ❯ nm -gp ./target/debug/librubicon.dylib | grep MERCHANDISE 368 | 00000000000099f0 S _STILL_MERCHANDISE 369 | ``` 370 | 371 | However, when importing, there is no way to opt into mangling. 372 | 373 | We can either import it as-is, without mangling: 374 | 375 | ```rust 376 | extern "C" { 377 | static MERCHANDISE: u64; 378 | } 379 | 380 | // (only here to force the linker to import MERCHANDISE) 381 | #[used] 382 | static MERCHANDISE_ADDR: &u64 = unsafe { &MERCHANDISE }; 383 | ``` 384 | 385 | ```shell 386 | # needed to avoid link errors: `MERCHANDISE` is not present at link time, it's 387 | # only expected to be present at load time. 388 | ❯ export RUSTFLAGS="-Clink-arg=-undefined -Clink-arg=dynamic_lookup" 389 | 390 | ❯ cargo build --quiet 391 | ❯ nm -gp ./target/debug/librubicon.dylib | grep MERCHANDISE 392 | 00000000000e0210 S __ZN7rubicon16MERCHANDISE_ADDR17h2755f244419dcf79E 393 | U _MERCHANDISE 394 | ``` 395 | 396 | Or we can specify a `link_name` explicitly: 397 | 398 | ```rust 399 | extern "C" { 400 | #[link_name = "STILL_MERCHANDISE"] 401 | static MERCHANDISE: u64; 402 | } 403 | 404 | // (only here to force the linker to import MERCHANDISE) 405 | #[used] 406 | static MERCHANDISE_ADDR: &u64 = unsafe { &MERCHANDISE }; 407 | ``` 408 | 409 | ```shell 410 | 00000000000e0210 S __ZN7rubicon16MERCHANDISE_ADDR17h2755f244419dcf79E 411 | U _STILL_MERCHANDISE 412 | ``` 413 | 414 | All these alternatives, quite frankly, suck. 415 | 416 | If we opt into mangling, we're safe from name collisions, but we _cannot_ import 417 | that symbol again (I'm not counting "manually copying and pasting the mangled name 418 | into Rust source code"). 419 | 420 | If we opt out of mangling, two crates that export `CURRENT_STATE` will clash. 421 | 422 | In practice, we have no choice but to opt out of mangling, and make sure there's no 423 | collision between the unmangled globals of various crates in the dependency graph — 424 | which means, that's right, we're back to manually prefixing things, like in C. 425 | 426 | We've just covered process-locals. The situation for thread-locals is much the 427 | same, except we have to do some more trickery because the internals of `LocalKey` 428 | are, well, internal, and cannot be accessed from stable Rust. 429 | 430 | Getting all these just right is tricky — that's why `rubicon` ships macros, which 431 | are meant to be used by any crate that has global state, such as `tokio`, `tracing`, 432 | `parking_lot`, etc. 433 | 434 | This is not as good as a rustc flag, but it's all we got right now. In time, the 435 | hope is that `rubicon` will disappear. 436 | 437 | ## Making a crate rubicon-compatible 438 | 439 | If you maintain a crate that has global state, you might want to make it 440 | rubicon-compatible. 441 | 442 | ### Depend on rubicon 443 | 444 | You'll need to add a non-optional dependency to it: 445 | 446 | ```shell 447 | cargo add rubicon 448 | ``` 449 | 450 | Without any features added, it has zero dependencies. 451 | 452 | When `rubicon/import-globals` or `rubicon/export-globals` is enabled, it will 453 | pull in [paste](https://crates.io/crates/paste), which is a proc-macro: I'm not 454 | fond of the idea, but I've explored alternatives and token pasting is the best 455 | I can do right now. 456 | 457 | Enabling _both_ features at the same time will yield a compile error, and 458 | enabling _neither_ will act as if your crate wasn't using rubicon's macros at 459 | all (so most users of your crate should be completely unaffected). 460 | 461 | Users are in charge of adding their _own_ dependency to `rubicon` and enabling 462 | either feature — this avoids feature proliferation. Provided that there's only one 463 | copy of `rubicon` in the entire depgraph (e.g. everyone is on 3.x), then the scheme 464 | works. 465 | 466 | ### Macro your thread-locals 467 | 468 | `rubicon::thread_local!` is a drop-in replacement for `std::thread_local!`. 469 | 470 | Before: 471 | 472 | ```rust 473 | std::thread_local! { 474 | static BUF: RefCell = RefCell::new(String::new()); 475 | } 476 | ``` 477 | 478 | After: 479 | 480 | ```rust 481 | rubicon::thread_local! { 482 | static BUF: RefCell = RefCell::new(String::new()); 483 | } 484 | ``` 485 | 486 | However, keep in mind that, whenever import/export is enabled, mangling will 487 | be disabled for your static. Thus, it might be a good idea to preemptively 488 | prefix it: 489 | 490 | ```rust 491 | rubicon::thread_local! { 492 | static MY_CRATE_BUF: RefCell = RefCell::new(String::new()); 493 | } 494 | ``` 495 | 496 | ### Macro your statics 497 | 498 | Before: 499 | 500 | ```rust 501 | static DISPATCHERS: Dispatchers = Dispatchers::new(); 502 | static CALLSITES: Callsites = Callsites { 503 | list_head: AtomicPtr::new(ptr::null_mut()), 504 | has_locked_callsites: AtomicBool::new(false), 505 | }; 506 | static DISPATCHERS: Dispatchers = Dispatchers::new(); 507 | static LOCKED_CALLSITES: Lazy>> = Lazy::new(Default::default); 508 | ``` 509 | 510 | After: 511 | 512 | ```rust 513 | rubicon::process_local! { 514 | static DISPATCHERS: Dispatchers = Dispatchers::new(); 515 | static CALLSITES: Callsites = Callsites { 516 | list_head: AtomicPtr::new(ptr::null_mut()), 517 | has_locked_callsites: AtomicBool::new(false), 518 | }; 519 | static DISPATCHERS: Dispatchers = Dispatchers::new(); 520 | static LOCKED_CALLSITES: Lazy>> = Lazy::new(Default::default); 521 | } 522 | ``` 523 | 524 | Both `thread_local!` and `process_local!` support multiple definitions. 525 | 526 | In addition, `process_local!` supports `static mut`, should you _really_ need it (looking 527 | at you tracing-core). 528 | 529 | ### Mind your dependencies 530 | 531 | Sometimes thread-locals and statics hide in the darndest of places. 532 | 533 | For example, `tokio` depends on `parking_lot` which has global state (did you know?) 534 | 535 | ```rust 536 | /// Holds the pointer to the currently active `HashTable`. 537 | /// 538 | /// # Safety 539 | /// 540 | /// Except for the initial value of null, it must always point to a valid `HashTable` instance. 541 | /// Any `HashTable` this global static has ever pointed to must never be freed. 542 | static PARKING_LOT_HASHTABLE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); 543 | ``` 544 | 545 | ## Implementing the `xgraph` model 546 | 547 | Assuming all your dependencies are rubicon-compatible, you can implement the `xgraph` model! 548 | 549 | In terms of crates, you'll need 550 | 551 | * `bin`, a bin crate, depends on `exports`, and `libloading` 552 | * `exports`, a lib crate, `crate-type=["dylib"]` (that's just "dye lib") 553 | * depends on _all_ your rubicon-compatible dependencies 554 | * depends on `rubicon` with feature `export-globals` enabled 555 | * `mod_a`, a lib crate, `crate-type=["cdylib"]` (that's "see dye lib") 556 | * depends on `rubicon` with feature `import-globals` enabled 557 | * `mod_b`, like `mod_a` 558 | * `mod_c`, like `mod_a` 559 | * etc. 560 | 561 | > The `exports` crate is needed to bring all globals in the address space in a way 562 | > that the dynamic linker can understand. 563 | > 564 | > _Technically_ `-rdynamic` should help there, but I couldn't get it to work. 565 | 566 | That's about it. Don't forget the invariants! 567 | 568 | * A. Modules are NEVER UNLOADED, only loaded. 569 | * B. The EXACT SAME RUSTC VERSION is used to build the app and all modules 570 | * C. The EXACT SAME CARGO FEATURES are enabled for crates that both the app 571 | and some modules depend on. 572 | 573 | You can find a full example in `test-crates/` in [the rubicon repository](https://github.com/bearcove/rubicon). 574 | 575 | ## License 576 | 577 | This project is primarily distributed under the terms of both the MIT license 578 | and the Apache License (Version 2.0). 579 | 580 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details. 581 | -------------------------------------------------------------------------------- /rubicon/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [3.4.9](https://github.com/bearcove/rubicon/compare/v3.4.8...v3.4.9) - 2024-09-17 10 | 11 | ### Fixed 12 | 13 | - Make globals uppercase I guess 14 | 15 | ## [3.4.8](https://github.com/bearcove/rubicon/compare/v3.4.7...v3.4.8) - 2024-09-17 16 | 17 | ### Fixed 18 | 19 | - Improve compatibility_check_once 20 | 21 | ## [3.4.7](https://github.com/bearcove/rubicon/compare/v3.4.6...v3.4.7) - 2024-09-17 22 | 23 | ### Other 24 | 25 | - Explain running compat checks locally 26 | - Export compatibility_check_once no matter what cargo features are enabled 27 | 28 | ## [3.4.6](https://github.com/bearcove/rubicon/compare/v3.4.5...v3.4.6) - 2024-09-17 29 | 30 | ### Other 31 | 32 | - Add missing words in docs 33 | 34 | ## [3.4.5](https://github.com/bearcove/rubicon/compare/v3.4.4...v3.4.5) - 2024-09-17 35 | 36 | ### Other 37 | 38 | - Add logo to docs page 39 | 40 | ## [3.4.4](https://github.com/bearcove/rubicon/compare/v3.4.3...v3.4.4) - 2024-09-17 41 | 42 | ### Other 43 | 44 | - Fix failing doc tests 45 | - Improve documentation a lot 46 | 47 | ## [3.4.3](https://github.com/bearcove/rubicon/compare/v3.4.2...v3.4.3) - 2024-09-17 48 | 49 | ### Other 50 | 51 | - Upgrade misiasart website 52 | 53 | ## [3.4.2](https://github.com/bearcove/rubicon/compare/v3.4.1...v3.4.2) - 2024-09-14 54 | 55 | ### Other 56 | 57 | - Have macros generate attributes that let clippy ignore 58 | 59 | ## [3.4.1](https://github.com/bearcove/rubicon/compare/v3.4.0...v3.4.1) - 2024-09-05 60 | 61 | ### Other 62 | - Update logo attribution 63 | - Add license 64 | - [ci skip] logo fix 65 | - Newer, better, logo! 66 | - capitalization 67 | - dagnerous-- 68 | - Add logo 69 | 70 | ## [3.4.0](https://github.com/bearcove/rubicon/compare/v3.3.5...v3.4.0) - 2024-08-02 71 | 72 | ### Added 73 | - Introduce --no-compatibility-checks-yolo, for when you uhh want speed? 74 | 75 | ## [3.3.5](https://github.com/bearcove/rubicon/compare/v3.3.4...v3.3.5) - 2024-08-01 76 | 77 | ### Other 78 | - Don't make rubicon a dylib? 79 | 80 | ## [3.3.4](https://github.com/bearcove/rubicon/compare/v3.3.3...v3.3.4) - 2024-07-30 81 | 82 | ### Fixed 83 | - Remove debug prints that slipped in 84 | -------------------------------------------------------------------------------- /rubicon/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "libc" 7 | version = "0.2.155" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 10 | 11 | [[package]] 12 | name = "paste" 13 | version = "1.0.15" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 16 | 17 | [[package]] 18 | name = "rubicon" 19 | version = "3.4.9" 20 | dependencies = [ 21 | "libc", 22 | "paste", 23 | "rustc_version", 24 | ] 25 | 26 | [[package]] 27 | name = "rustc_version" 28 | version = "0.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 31 | dependencies = [ 32 | "semver", 33 | ] 34 | 35 | [[package]] 36 | name = "semver" 37 | version = "1.0.23" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 40 | -------------------------------------------------------------------------------- /rubicon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rubicon" 3 | version = "3.4.9" 4 | edition = "2021" 5 | authors = ["Amos Wenger "] 6 | license = "MIT OR Apache-2.0" 7 | readme = "../README.md" 8 | repository = "https://github.com/bearcove/rubicon" 9 | description = "Deduplicate globals across shared objects to enable a dangerous form of dynamic linking" 10 | categories = ["development-tools::ffi"] 11 | keywords = ["ffi", "thread-local"] 12 | 13 | [dependencies] 14 | libc = { version = "0.2.155", optional = true } 15 | paste = { version = "1.0.15", optional = true } 16 | 17 | [build-dependencies] 18 | rustc_version = { version = "0.4.0", optional = true } 19 | 20 | [features] 21 | default = [] 22 | export-globals = ["dep:paste", "dep:rustc_version"] 23 | import-globals = ["dep:paste", "dep:rustc_version", "dep:libc"] 24 | no-compatibility-checks-yolo = [] 25 | -------------------------------------------------------------------------------- /rubicon/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 3 | { 4 | use std::env; 5 | 6 | // Get the Rust compiler version and set it as an environment variable. 7 | let rustc_version = rustc_version::version().unwrap(); 8 | println!("cargo:rustc-env=RUBICON_RUSTC_VERSION={}", rustc_version); 9 | 10 | // Pass the target triple. 11 | let target = env::var("TARGET").unwrap(); 12 | println!("cargo:rustc-env=RUBICON_TARGET_TRIPLE={}", target); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rubicon/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ![The rubicon logo: a shallow river in northeastern Italy famously crossed by Julius Caesar in 49 BC](https://github.com/user-attachments/assets/7e10888d-9f44-4395-a2ad-3e3fc0801996) 2 | //! 3 | //! _Logo by [MisiasArt](https://misiasart.com)_ 4 | //! 5 | //! rubicon enables a dangerous form of dynamic linking in Rust through cdylib crates 6 | //! and carefully-enforced invariants. 7 | //! 8 | //! This crate provides macros to handle global state (thread-locals and process-locals/statics) 9 | //! in a way that's compatible with the "xgraph" dynamic linking model, where multiple 10 | //! copies of the same crate can coexist in the same address space. 11 | //! 12 | //! The main macros provided are: 13 | //! 14 | //! - [`thread_local!`]: A drop-in replacement for [`std::thread_local!`] 15 | //! - [`process_local!`]: Used to declare statics (including `static mut`) 16 | //! 17 | //! These macros behave differently depending on which feature is enabled: 18 | //! 19 | //! - `export-globals`: symbols are exported for use by other shared objects 20 | //! - `import-globals`: symbols are imported from "the dynamic loader namespace" 21 | //! - neither: the macros act as pass-through to standard Rust constructs 22 | //! 23 | //! Additionally, the [`compatibility_check!`] macro is provided to help ensure that 24 | //! common dependencies used by various shared objects are ABI-compatible. 25 | //! 26 | //! ## Explain like I'm five 27 | //! 28 | //! Let's assume you're a very precocious five-year old: say you're making a 29 | //! static site generator. Part of its job is to compile LaTeX markup into HTML: 30 | //! this uses KaTeX, which requires a JavaScript runtime, that takes a long time 31 | //! to compile. 32 | //! 33 | //! You decide you want to put this functionality in a shared object, so that you 34 | //! can iterate on the _rest_ of the static site generator without the whole JS 35 | //! runtime being recompiled every time, or even taken into account by cargo when 36 | //! doing check/clippy/build/test/etc. 37 | //! 38 | //! However, both your app and your "latex module" use the [tracing](https://crates.io/crates/tracing) 39 | //! crate for structured logging. tracing uses "globals" (thread-locals and process-locals) to 40 | //! keep track of the current span, and where to log events (ie. the "subscriber"). 41 | //! 42 | //! If you do `tracing_subscriber::fmt::init()` from the app, any use of `tracing` in the app 43 | //! will work fine, but if you do the same from the module, the log events will go nowhere: 44 | //! as far as it's concerned (because it has a copy of the entire code of `tracing`), there 45 | //! _is_ no subscriber. 46 | //! 47 | //! This is where `rubicon` comes in: by patching `tracing` to use rubicon's macros, like 48 | //! [`thread_local!`] and [`process_local!`], we can have the app _export_ the globals, and 49 | //! the module _import_ them, so that there's only one "global subscriber" for all shared 50 | //! objects. 51 | //! 52 | //! ## That's it? 53 | //! 54 | //! Not quite — it's actually annoyingly hard to export symbols from an executable. So really 55 | //! what you have instead is a `rubicon-exports` shared object that both the app and the module 56 | //! link against, and import all globals from. 57 | //! 58 | //! ## Why isn't this built into rustc/cargo? 59 | //! 60 | //! Because of the "Safety" section below. However, I believe if we work together, 61 | //! we can make this crate redundant. A global `-C globals-linkage=[import,export]` 62 | //! rustc flag would singlehandedly solve the problem. 63 | //! 64 | //! Someone just has to do it. In the meantime, this crate (and source-patching crates like 65 | //! `tokio`, `tracing`, `parking_lot`, `eyre`, see the [compatibility tracker](https://github.com/bearcove/rubicon/issues/3)) 66 | //! act as a "polyfill" — proving that it's a reasonable and useful approach, which should 67 | //! make "hey can we let rustc dangerously allow dynamic linking" simpler to defend when it 68 | //! comes time to submit a PR. 69 | //! 70 | //! ## Safety 71 | //! 72 | //! By using this crate, you agree to: 73 | //! 74 | //! 1. Use the exact same rustc version for all shared objects 75 | //! 2. Not use [`-Z randomize-layout`](https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/randomize-layout.html#randomize-layout) (duh) 76 | //! 3. Enable the exact same cargo features for all common dependencies (e.g. `tokio`) 77 | //! 78 | //! In short: don't do anything that would cause crates to have a different ABI from one shared 79 | //! object to the next. 1 and 2 are trivial, as for 3, the [`compatibility_check!`] macro is here 80 | //! to help. 81 | //! 82 | //! For more details on the motivation and implementation of the "xgraph" model, 83 | //! refer to the [crate's README and documentation](https://github.com/bearcove/rubicon?tab=readme-ov-file#rubicon). 84 | 85 | #[cfg(all(feature = "export-globals", feature = "import-globals"))] 86 | compile_error!("The features `export-globals` and `import-globals` are mutually exclusive, see https://github.com/bearcove/rubicon"); 87 | 88 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 89 | pub use paste::paste; 90 | 91 | #[cfg(feature = "import-globals")] 92 | pub use libc; 93 | 94 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 95 | pub const RUBICON_RUSTC_VERSION: &str = env!("RUBICON_RUSTC_VERSION"); 96 | 97 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 98 | pub const RUBICON_TARGET_TRIPLE: &str = env!("RUBICON_TARGET_TRIPLE"); 99 | 100 | //============================================================================== 101 | // Wrappers 102 | //============================================================================== 103 | 104 | /// Wrapper around an `extern` `static` ref to avoid requiring `unsafe` for imported globals. 105 | #[doc(hidden)] 106 | pub struct TrustedExtern(pub &'static T, pub fn()); 107 | 108 | use std::ops::Deref; 109 | 110 | impl Deref for TrustedExtern { 111 | type Target = T; 112 | 113 | #[inline(always)] 114 | fn deref(&self) -> &Self::Target { 115 | // this is a good time to run compatibility checks 116 | #[cfg(not(feature = "no-compatibility-checks-yolo"))] 117 | (self.1)(); 118 | 119 | self.0 120 | } 121 | } 122 | 123 | /// Wrapper around an `extern` `static` double-ref to avoid requiring `unsafe` for imported globals. 124 | /// 125 | /// The reason we have a double-ref is that when exporting thread-locals, the dynamic symbol is 126 | /// already a ref. Then, in our own static, we can only access the address of that ref, not its 127 | /// value (since its value is only known as load time, not compile time). 128 | /// 129 | /// As a result, imported thread-locals have an additional layer of indirection. 130 | #[doc(hidden)] 131 | pub struct TrustedExternDouble(pub &'static &'static T, pub fn()); 132 | 133 | impl Deref for TrustedExternDouble { 134 | type Target = T; 135 | 136 | #[inline(always)] 137 | fn deref(&self) -> &Self::Target { 138 | // this is a good time to run compatibility checks 139 | #[cfg(not(feature = "no-compatibility-checks-yolo"))] 140 | (self.1)(); 141 | 142 | self.0 143 | } 144 | } 145 | 146 | //============================================================================== 147 | // Thread-locals 148 | //============================================================================== 149 | 150 | /// A drop-in replacement for [`std::thread_local`] that imports/exports the 151 | /// thread-local, depending on the enabled cargo features. 152 | /// 153 | /// Before: 154 | /// 155 | /// ```rust 156 | /// # use std::sync::atomic::AtomicU32; 157 | /// std::thread_local! { 158 | /// static FOO: AtomicU32 = AtomicU32::new(42); 159 | /// } 160 | /// ``` 161 | /// 162 | /// After: 163 | /// 164 | /// ```rust 165 | /// # use std::sync::atomic::AtomicU32; 166 | /// rubicon::thread_local! { 167 | /// static FOO: AtomicU32 = AtomicU32::new(42); 168 | /// } 169 | /// ``` 170 | /// 171 | /// This will import `FOO` if the `import-globals` feature is enabled, and export it if the 172 | /// `export-globals` feature is enabled. 173 | /// 174 | /// rubicon tries to be non-obtrusive: when neither feature is enabled, the macro 175 | /// forwards to [`std::thread_local`], resulting in no performance penalty, 176 | /// no difference in binary size, etc. 177 | /// 178 | /// ## Name mangling, collisions 179 | /// 180 | /// When the `import-globals` or `export-globals` feature is enabled, name mangling 181 | /// will be disabled for thread-locals declared through this macro (due to unfortunate 182 | /// limitations of the Rust attributes used to implement this). 183 | /// 184 | /// We recommend prefixing your thread-locals with your crate/module name to 185 | /// avoid collisions: 186 | /// 187 | /// ```rust 188 | /// # use std::sync::atomic::AtomicU32; 189 | /// rubicon::thread_local! { 190 | /// static MY_CRATE_FOO: AtomicU32 = AtomicU32::new(42); 191 | /// } 192 | /// ``` 193 | /// 194 | /// ## Multiple declarations 195 | /// 196 | /// This macro supports multiple declarations in the same invocation, just like 197 | /// [`std::thread_local`] would: 198 | /// 199 | /// ```rust 200 | /// # use std::sync::atomic::AtomicU32; 201 | /// rubicon::thread_local! { 202 | /// static FOO: AtomicU32 = AtomicU32::new(42); 203 | /// static BAR: AtomicU32 = AtomicU32::new(43); 204 | /// } 205 | /// ``` 206 | #[cfg(not(any(feature = "import-globals", feature = "export-globals")))] 207 | #[macro_export] 208 | macro_rules! thread_local { 209 | ($($tts:tt)+) => { 210 | ::std::thread_local!{ $($tts)+ } 211 | } 212 | } 213 | 214 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 215 | #[macro_export] 216 | macro_rules! thread_local { 217 | // empty (base case for the recursion) 218 | () => {}; 219 | 220 | ($(#[$attrs:meta])* $vis:vis static $name:ident: $ty:ty = const { $expr:expr } $(;)?) => { 221 | $crate::thread_local! { 222 | $(#[$attrs])* 223 | $vis static $name: $ty = $expr; 224 | } 225 | }; 226 | 227 | ($(#[$attrs:meta])* $vis:vis static $name:ident: $ty:ty = $expr:expr $(;)?) => { 228 | $crate::thread_local_inner!($(#[$attrs])* $vis $name, $ty, $expr); 229 | }; 230 | 231 | // handle multiple declarations 232 | ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => ( 233 | $crate::thread_local_inner!($(#[$attr])* $vis $name, $t, $init); 234 | $crate::thread_local!($($rest)*); 235 | ); 236 | } 237 | 238 | #[cfg(feature = "export-globals")] 239 | #[macro_export] 240 | macro_rules! thread_local_inner { 241 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 242 | $crate::paste! { 243 | // regular thread-local macro, not exported. 244 | ::std::thread_local! { 245 | $(#[$attrs])* 246 | $vis static $name: $ty = $expr; 247 | } 248 | 249 | #[no_mangle] 250 | #[allow(clippy::non_upper_case_globals)] 251 | static [<$name __RUBICON_EXPORT>]: &::std::thread::LocalKey<$ty> = &$name; 252 | } 253 | }; 254 | } 255 | 256 | #[cfg(feature = "import-globals")] 257 | #[macro_export] 258 | #[allow(clippy::crate_in_macro_def)] // we _do_ mean the invocation site's crate, not the macro's 259 | macro_rules! thread_local_inner { 260 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 261 | $crate::paste! { 262 | extern "Rust" { 263 | #[link_name = stringify!([<$name __RUBICON_EXPORT>])] 264 | #[allow(improper_ctypes)] 265 | #[allow(clippy::non_upper_case_globals)] 266 | static [<$name __RUBICON_IMPORT>]: &'static ::std::thread::LocalKey<$ty>; 267 | } 268 | 269 | // even though this ends up being not a LocalKey, but a type that Derefs to LocalKey, 270 | // in practice, most codebases work just fine with this, since they call methods 271 | // that takes `self: &LocalKey`: they don't see the difference. 272 | $vis static $name: $crate::TrustedExternDouble<::std::thread::LocalKey<$ty>> = $crate::TrustedExternDouble(unsafe { &[<$name __RUBICON_IMPORT>] }, crate::compatibility_check_once); 273 | } 274 | }; 275 | } 276 | 277 | //============================================================================== 278 | // Process-locals (statics) 279 | //============================================================================== 280 | 281 | /// Imports or exports a `static`, depending on the enabled cargo features. 282 | /// 283 | /// Before: 284 | /// 285 | /// ```rust 286 | /// static FOO: u32 = 42; 287 | /// ``` 288 | /// 289 | /// After: 290 | /// 291 | /// ```rust 292 | /// rubicon::process_local! { 293 | /// static FOO: u32 = 42; 294 | /// } 295 | /// ``` 296 | /// 297 | /// This will import `FOO` if the `import-globals` feature is enabled, and export it if the 298 | /// `export-globals` feature is enabled. 299 | /// 300 | /// rubicon tries to be non-obtrusive: when neither feature is enabled, the macro 301 | /// will expand to the static declaration itself, resulting in no performance penalty, 302 | /// no difference in binary size, etc. 303 | /// 304 | /// ## Name mangling, collisions 305 | /// 306 | /// When the `import-globals` or `export-globals` feature is enabled, name mangling 307 | /// will be disabled for process-locals declared through this macro (due to unfortunate 308 | /// limitations of the Rust attributes used to implement this). 309 | /// 310 | /// We recommend prefixing your process-locals with your crate/module name to 311 | /// avoid collisions: 312 | /// 313 | /// ```rust 314 | /// rubicon::process_local! { 315 | /// static MY_CRATE_FOO: u32 = 42; 316 | /// } 317 | /// ``` 318 | /// 319 | /// ## Multiple declarations, `mut` 320 | /// 321 | /// This macro supports multiple declarations, along with `static mut` declarations 322 | /// (which have a slightly different expansion). 323 | /// 324 | /// ```rust 325 | /// # use std::sync::atomic::AtomicU32; 326 | /// # struct Dispatcher; 327 | /// # impl Dispatcher { 328 | /// # const fn new() -> Self { Self } 329 | /// # } 330 | /// rubicon::process_local! { 331 | /// static FOO: AtomicU32 = AtomicU32::new(42); 332 | /// static mut BAR: Dispatcher = Dispatcher::new(); 333 | /// } 334 | /// ``` 335 | /// 336 | /// If you're curious about the exact macro expansion, ask rust-analyzer to 337 | /// expand it for you via its [Expand Macro Recursively](https://rust-analyzer.github.io/manual.html#expand-macro-recursively) 338 | /// functionalityl. 339 | #[cfg(all(not(feature = "import-globals"), not(feature = "export-globals")))] 340 | #[macro_export] 341 | macro_rules! process_local { 342 | // pass through 343 | ($($tts:tt)+) => { 344 | $($tts)+ 345 | } 346 | } 347 | 348 | #[cfg(any(feature = "export-globals", feature = "import-globals"))] 349 | #[macro_export] 350 | macro_rules! process_local { 351 | // empty (base case for the recursion) 352 | () => {}; 353 | 354 | // single declaration 355 | ($(#[$attrs:meta])* $vis:vis static $name:ident: $ty:ty = $expr:expr $(;)?) => { 356 | $crate::process_local_inner!($(#[$attrs])* $vis $name, $ty, $expr); 357 | }; 358 | 359 | // single declaration (mut) 360 | ($(#[$attrs:meta])* $vis:vis static mut $name:ident: $ty:ty = $expr:expr $(;)?) => { 361 | $crate::process_local_inner_mut!($(#[$attrs])* $vis $name, $ty, $expr); 362 | }; 363 | 364 | 365 | // handle multiple declarations 366 | ($(#[$attrs:meta])* $vis:vis static $name:ident: $ty:ty = $expr:expr; $($rest:tt)*) => { 367 | $crate::process_local_inner!($(#[$attrs])* $vis $name, $ty, $expr); 368 | $crate::process_local!($($rest)*); 369 | }; 370 | 371 | // handle multiple declarations 372 | ($(#[$attrs:meta])* $vis:vis static mut $name:ident: $ty:ty = $expr:expr; $($rest:tt)*) => { 373 | $crate::process_local_inner_mut!($(#[$attrs])* $vis $name, $ty, $expr); 374 | $crate::process_local!($($rest)*); 375 | } 376 | } 377 | 378 | #[cfg(feature = "export-globals")] 379 | #[macro_export] 380 | macro_rules! process_local_inner { 381 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 382 | $crate::paste! { 383 | #[export_name = stringify!([<$name __RUBICON_EXPORT>])] 384 | $(#[$attrs])* 385 | $vis static $name: $ty = $expr; 386 | } 387 | }; 388 | } 389 | 390 | #[cfg(feature = "export-globals")] 391 | #[macro_export] 392 | macro_rules! process_local_inner_mut { 393 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 394 | $crate::paste! { 395 | #[export_name = stringify!([<$name __RUBICON_EXPORT>])] 396 | $(#[$attrs])* 397 | $vis static mut $name: $ty = $expr; 398 | } 399 | }; 400 | } 401 | 402 | #[cfg(feature = "import-globals")] 403 | #[macro_export] 404 | #[allow(clippy::crate_in_macro_def)] // we _do_ mean the invocation site's crate, not the macro's 405 | macro_rules! process_local_inner { 406 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 407 | $crate::paste! { 408 | extern "Rust" { 409 | #[link_name = stringify!([<$name __RUBICON_EXPORT>])] 410 | #[allow(improper_ctypes)] 411 | #[allow(clippy::non_upper_case_globals)] 412 | static [<$name __RUBICON_IMPORT>]: $ty; 413 | } 414 | 415 | $vis static $name: $crate::TrustedExtern<$ty> = $crate::TrustedExtern(unsafe { &[<$name __RUBICON_IMPORT>] }, crate::compatibility_check_once); 416 | } 417 | }; 418 | } 419 | 420 | #[cfg(feature = "import-globals")] 421 | #[macro_export] 422 | macro_rules! process_local_inner_mut { 423 | ($(#[$attrs:meta])* $vis:vis $name:ident, $ty:ty, $expr:expr) => { 424 | $crate::paste! { 425 | // externs require "unsafe" to access, but so do "static mut", so, 426 | // no need to wrap in `TrustedExtern` 427 | extern "Rust" { 428 | #[link_name = stringify!([<$name __RUBICON_EXPORT>])] 429 | #[allow(improper_ctypes)] 430 | $vis static mut $name: $ty; 431 | } 432 | } 433 | }; 434 | } 435 | 436 | //============================================================================== 437 | // Compatibility check 438 | //============================================================================== 439 | 440 | #[cfg(feature = "export-globals")] 441 | #[macro_export] 442 | macro_rules! compatibility_check { 443 | ($($feature:tt)*) => { 444 | use std::env; 445 | 446 | $crate::paste! { 447 | #[no_mangle] 448 | #[export_name = concat!(env!("CARGO_PKG_NAME"), "_compatibility_info")] 449 | static __RUBICON_COMPATIBILITY_INFO_: &'static [(&'static str, &'static str)] = &[ 450 | ("rustc-version", $crate::RUBICON_RUSTC_VERSION), 451 | ("target-triple", $crate::RUBICON_TARGET_TRIPLE), 452 | $($feature)* 453 | ]; 454 | } 455 | 456 | pub fn compatibility_check_once() { 457 | // no-op when exporting 458 | } 459 | }; 460 | } 461 | 462 | #[cfg(all(unix, feature = "import-globals"))] 463 | #[macro_export] 464 | macro_rules! compatibility_check { 465 | ($($feature:tt)*) => { 466 | use std::env; 467 | 468 | extern "Rust" { 469 | #[link_name = concat!(env!("CARGO_PKG_NAME"), "_compatibility_info")] 470 | static COMPATIBILITY_INFO: &'static [(&'static str, &'static str)]; 471 | } 472 | 473 | 474 | fn get_shared_object_name() -> Option { 475 | use $crate::libc::{c_void, Dl_info}; 476 | use std::ffi::CStr; 477 | use std::ptr; 478 | 479 | extern "C" { 480 | fn dladdr(addr: *const c_void, info: *mut Dl_info) -> i32; 481 | } 482 | 483 | unsafe { 484 | let mut info: Dl_info = std::mem::zeroed(); 485 | if dladdr(get_shared_object_name as *const c_void, &mut info) != 0 { 486 | let c_str = CStr::from_ptr(info.dli_fname); 487 | return Some(c_str.to_string_lossy().into_owned()); 488 | } 489 | } 490 | None 491 | } 492 | 493 | struct AnsiEscape(u64, D); 494 | 495 | impl std::fmt::Display for AnsiEscape { 496 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 497 | let inner = format!("\x1b[{}m{}\x1b[0m", self.0, self.1); 498 | f.pad(&inner) 499 | } 500 | } 501 | 502 | #[derive(Clone, Copy)] 503 | struct AnsiColor(u64); 504 | 505 | impl AnsiColor { 506 | const BLUE: AnsiColor = AnsiColor(34); 507 | const GREEN: AnsiColor = AnsiColor(32); 508 | const RED: AnsiColor = AnsiColor(31); 509 | const GREY: AnsiColor = AnsiColor(37); 510 | } 511 | 512 | fn colored(color: AnsiColor, d: D) -> AnsiEscape { 513 | AnsiEscape(color.0, d) 514 | } 515 | fn blue(d: D) -> AnsiEscape { 516 | AnsiEscape(34, d) 517 | } 518 | fn green(d: D) -> AnsiEscape { 519 | AnsiEscape(32, d) 520 | } 521 | fn red(d: D) -> AnsiEscape { 522 | AnsiEscape(31, d) 523 | } 524 | fn grey(d: D) -> AnsiEscape { 525 | AnsiEscape(35, d) 526 | } 527 | 528 | // Helper function to count visible characters (ignoring ANSI escapes) 529 | fn visible_len(s: &str) -> usize { 530 | let mut len = 0; 531 | let mut in_escape = false; 532 | for c in s.chars() { 533 | if c == '\x1b' { 534 | in_escape = true; 535 | } else if in_escape { 536 | if c.is_alphabetic() { 537 | in_escape = false; 538 | } 539 | } else { 540 | len += 1; 541 | } 542 | } 543 | len 544 | } 545 | 546 | pub fn compatibility_check_once() { 547 | fn check_compatibility() { 548 | let imported: &[(&str, &str)] = &[ 549 | ("rustc-version", $crate::RUBICON_RUSTC_VERSION), 550 | ("target-triple", $crate::RUBICON_TARGET_TRIPLE), 551 | $($feature)* 552 | ]; 553 | let exported = unsafe { COMPATIBILITY_INFO }; 554 | 555 | let missing: Vec<_> = imported.iter().filter(|&item| !exported.contains(item)).collect(); 556 | let extra: Vec<_> = exported.iter().filter(|&item| !imported.contains(item)).collect(); 557 | 558 | if missing.is_empty() && extra.is_empty() { 559 | // all good 560 | return; 561 | } 562 | 563 | let so_name = get_shared_object_name().unwrap_or("unknown_so".to_string()); 564 | // get only the last bit of the path 565 | let so_name = so_name.rsplit('/').next().unwrap_or("unknown_so"); 566 | 567 | let exe_name = std::env::current_exe().map(|p| p.file_name().unwrap().to_string_lossy().to_string()).unwrap_or_else(|_| "unknown_exe".to_string()); 568 | 569 | let mut error_message = String::new(); 570 | error_message.push_str("\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n"); 571 | error_message.push_str(&format!(" 💀 Feature mismatch for crate \x1b[31m{}\x1b[0m\n\n", env!("CARGO_PKG_NAME"))); 572 | 573 | error_message.push_str(&format!("{} has an incompatible configuration for {}.\n\n", blue(so_name), red(env!("CARGO_PKG_NAME")))); 574 | 575 | // Compute max lengths for alignment 576 | let max_exported_len = exported.iter().map(|(k, v)| format!("{}={}", k, v).len()).max().unwrap_or(0); 577 | let max_ref_len = imported.iter().map(|(k, v)| format!("{}={}", k, v).len()).max().unwrap_or(0); 578 | let column_width = max_exported_len.max(max_ref_len); 579 | 580 | // Gather all unique keys 581 | let mut all_keys: Vec<&str> = Vec::new(); 582 | for (key, _) in exported.iter() { 583 | if !all_keys.contains(key) { 584 | all_keys.push(key); 585 | } 586 | } 587 | for (key, _) in imported.iter() { 588 | if !all_keys.contains(key) { 589 | all_keys.push(key); 590 | } 591 | } 592 | 593 | struct Grid { 594 | rows: Vec>, 595 | column_widths: Vec, 596 | } 597 | 598 | impl Grid { 599 | fn new() -> Self { 600 | Grid { 601 | rows: Vec::new(), 602 | column_widths: Vec::new(), 603 | } 604 | } 605 | 606 | fn add_row(&mut self, row: Vec) { 607 | if self.column_widths.len() < row.len() { 608 | self.column_widths.resize(row.len(), 0); 609 | } 610 | for (i, cell) in row.iter().enumerate() { 611 | self.column_widths[i] = self.column_widths[i].max(visible_len(cell)); 612 | } 613 | self.rows.push(row); 614 | } 615 | 616 | fn write_to(&self, out: &mut String) { 617 | let total_width: usize = self.column_widths.iter().sum::() + self.column_widths.len() * 3 - 1; 618 | 619 | // Top border 620 | out.push_str(&format!("┌{}┐\n", "─".repeat(total_width))); 621 | 622 | for (i, row) in self.rows.iter().enumerate() { 623 | if i == 1 { 624 | // Separator after header 625 | out.push_str(&format!("╞{}╡\n", "═".repeat(total_width))); 626 | } 627 | 628 | for (j, cell) in row.iter().enumerate() { 629 | out.push_str("│ "); 630 | out.push_str(cell); 631 | out.push_str(&" ".repeat(self.column_widths[j] - visible_len(cell))); 632 | out.push_str(" "); 633 | } 634 | out.push_str("│\n"); 635 | } 636 | 637 | // Bottom border 638 | out.push_str(&format!("└{}┘\n", "─".repeat(total_width))); 639 | } 640 | } 641 | 642 | let mut grid = Grid::new(); 643 | 644 | // Add header 645 | grid.add_row(vec!["Key".to_string(), format!("Binary {}", blue(&exe_name)), format!("Module {}", blue(so_name))]); 646 | 647 | for key in all_keys.iter() { 648 | let exported_value = exported.iter().find(|&(k, _)| k == key).map(|(_, v)| v); 649 | let imported_value = imported.iter().find(|&(k, _)| k == key).map(|(_, v)| v); 650 | 651 | let key_column = colored(AnsiColor::GREY, key).to_string(); 652 | let binary_column = format_column(exported_value.as_deref().copied(), imported_value.as_deref().copied(), AnsiColor::GREEN); 653 | let module_column = format_column(imported_value.as_deref().copied(), exported_value.as_deref().copied(), AnsiColor::RED); 654 | 655 | fn format_column(primary: Option<&str>, secondary: Option<&str>, highlight_color: AnsiColor) -> String { 656 | match primary { 657 | Some(value) => { 658 | if secondary.map_or(false, |v| v == value) { 659 | colored(AnsiColor::GREY, value).to_string() 660 | } else { 661 | colored(highlight_color, value).to_string() 662 | } 663 | }, 664 | None => colored(AnsiColor::RED, "∅").to_string(), 665 | } 666 | } 667 | 668 | grid.add_row(vec![key_column, binary_column, module_column]); 669 | } 670 | 671 | grid.write_to(&mut error_message); 672 | 673 | struct MessageBox { 674 | lines: Vec, 675 | max_width: usize, 676 | } 677 | 678 | impl MessageBox { 679 | fn new() -> Self { 680 | MessageBox { 681 | lines: Vec::new(), 682 | max_width: 0, 683 | } 684 | } 685 | 686 | fn add_line(&mut self, line: String) { 687 | self.max_width = self.max_width.max(visible_len(&line)); 688 | self.lines.push(line); 689 | } 690 | 691 | fn add_empty_line(&mut self) { 692 | self.lines.push(String::new()); 693 | } 694 | 695 | fn write_to(&self, out: &mut String) { 696 | let box_width = self.max_width + 4; 697 | 698 | out.push_str("\n"); 699 | out.push_str(&format!("┌{}┐\n", "─".repeat(box_width - 2))); 700 | 701 | for line in &self.lines { 702 | if line.is_empty() { 703 | out.push_str(&format!("│{}│\n", " ".repeat(box_width - 2))); 704 | } else { 705 | let visible_line_len = visible_len(line); 706 | let padding = " ".repeat(box_width - 4 - visible_line_len); 707 | out.push_str(&format!("│ {}{} │\n", line, padding)); 708 | } 709 | } 710 | 711 | out.push_str(&format!("└{}┘", "─".repeat(box_width - 2))); 712 | } 713 | } 714 | 715 | error_message.push_str("\nDifferent feature sets may result in different struct layouts, which\n"); 716 | error_message.push_str("would lead to memory corruption. Instead, we're going to panic now.\n\n"); 717 | 718 | error_message.push_str("More info: \x1b[4m\x1b[34mhttps://crates.io/crates/rubicon\x1b[0m\n"); 719 | 720 | let mut message_box = MessageBox::new(); 721 | message_box.add_line(format!("To fix this issue, {} needs to enable", blue(so_name))); 722 | message_box.add_line(format!("the same cargo features as {} for crate {}.", blue(&exe_name), red(env!("CARGO_PKG_NAME")))); 723 | message_box.add_empty_line(); 724 | message_box.add_line("\x1b[34mHINT:\x1b[0m".to_string()); 725 | message_box.add_line(format!("Run `cargo tree -i {} -e features` from both.", red(env!("CARGO_PKG_NAME")))); 726 | 727 | message_box.write_to(&mut error_message); 728 | error_message.push_str("\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n"); 729 | 730 | panic!("{}", error_message); 731 | } 732 | 733 | // this one is _actually_ meant to exist once per shared object 734 | static COMPATIBILITY_CHECK_ONCE: std::sync::Once = std::sync::Once::new(); 735 | COMPATIBILITY_CHECK_ONCE.call_once(|| { 736 | check_compatibility(); 737 | }); 738 | } 739 | }; 740 | } 741 | 742 | #[cfg(all(not(unix), feature = "import-globals"))] 743 | #[macro_export] 744 | macro_rules! compatibility_check { 745 | ($($feature:tt)*) => { 746 | pub fn compatibility_check_once() { 747 | // compatibility checks are only supported on unix-like system 748 | } 749 | }; 750 | } 751 | 752 | /// Performs a compatibility check for the crate when using Rubicon's dynamic linking features. 753 | /// 754 | /// This macro is mandatory when the `import-globals` feature is enabled (as of Rubicon 3.3.3). 755 | /// It exports information about the crate's version and enabled features, which is then used 756 | /// by the import macros to ensure compatibility between different shared objects. 757 | /// 758 | /// # Usage 759 | /// 760 | /// At a minimum, you should include the crate's version: 761 | /// 762 | /// ``` 763 | /// rubicon::compatibility_check! { 764 | /// ("version", env!("CARGO_PKG_VERSION")), 765 | /// } 766 | /// ``` 767 | /// 768 | /// For crates with feature flags that affect struct layouts, you should include those as well: 769 | /// 770 | /// ``` 771 | /// rubicon::compatibility_check! { 772 | /// ("version", env!("CARGO_PKG_VERSION")), 773 | /// #[cfg(feature = "my_feature")] 774 | /// ("my_feature", "enabled"), 775 | /// #[cfg(feature = "another_feature")] 776 | /// ("another_feature", "enabled"), 777 | /// } 778 | /// ``` 779 | /// 780 | /// # Why is this necessary? 781 | /// 782 | /// When using Rubicon for dynamic linking, different shared objects may handle the same structs. 783 | /// If these shared objects have different features enabled, it can lead to incompatible struct 784 | /// layouts, causing memory corruption and safety issues. 785 | /// 786 | /// For example, in the Tokio runtime, enabling different features like timers or file system 787 | /// support can change the internal structure of various components. If one shared object expects 788 | /// a struct with certain fields (due to its feature set) and another shared object operates on 789 | /// that struct with a different expectation, it can lead to undefined behavior. 790 | /// 791 | /// This macro ensures that all shared objects agree on the crate's configuration, preventing 792 | /// such mismatches. 793 | /// 794 | /// # Real-world example (from tokio) 795 | /// 796 | /// See [this pull request](https://github.com/bearcove/tokio/pull/2) 797 | /// 798 | /// ``` 799 | /// rubicon::compatibility_check! { 800 | /// ("version", env!("CARGO_PKG_VERSION")), 801 | /// #[cfg(feature = "fs")] 802 | /// ("fs", "enabled"), 803 | /// #[cfg(feature = "io-util")] 804 | /// ("io-util", "enabled"), 805 | /// #[cfg(feature = "io-std")] 806 | /// ("io-std", "enabled"), 807 | /// #[cfg(feature = "net")] 808 | /// ("net", "enabled"), 809 | /// #[cfg(feature = "process")] 810 | /// ("process", "enabled"), 811 | /// #[cfg(feature = "rt")] 812 | /// ("rt", "enabled"), 813 | /// #[cfg(feature = "rt-multi-thread")] 814 | /// ("rt-multi-thread", "enabled"), 815 | /// #[cfg(feature = "signal")] 816 | /// ("signal", "enabled"), 817 | /// #[cfg(feature = "sync")] 818 | /// ("sync", "enabled"), 819 | /// #[cfg(feature = "time")] 820 | /// ("time", "enabled"), 821 | /// } 822 | /// ``` 823 | /// 824 | /// # When does the check happen and what happens if it fails? 825 | /// 826 | /// The check happens at runtime, lazily, when a global imported from a rubicon-aware 827 | /// crate is accessed (behind a [`std::sync::Once`]). 828 | /// 829 | /// If the check fails, the process will panic with a message like: 830 | /// 831 | /// ```text 832 | /// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 833 | /// 💀 Feature mismatch for crate mokio 834 | /// 835 | /// libmod_b.dylib has an incompatible configuration for mokio. 836 | /// 837 | /// ┌──────────────────────────────────────────────────────────────────┐ 838 | /// │ Key │ Binary samplebin │ Module libmod_b.dylib │ 839 | /// ╞══════════════════════════════════════════════════════════════════╡ 840 | /// │ rustc-version │ 1.81.0 │ 1.81.0 │ 841 | /// │ target-triple │ aarch64-apple-darwin │ aarch64-apple-darwin │ 842 | /// │ mokio_pkg_version │ 0.1.0 │ 0.1.0 │ 843 | /// │ timer │ disabled │ enabled │ 844 | /// │ timer_is_disabled │ 1 │ ∅ │ 845 | /// └──────────────────────────────────────────────────────────────────┘ 846 | /// 847 | /// Different feature sets may result in different struct layouts, which 848 | /// would lead to memory corruption. Instead, we're going to panic now. 849 | /// 850 | /// More info: https://crates.io/crates/rubicon 851 | /// 852 | /// ┌───────────────────────────────────────────────────────┐ 853 | /// │ To fix this issue, libmod_b.dylib needs to enable │ 854 | /// │ the same cargo features as samplebin for crate mokio. │ 855 | /// │ │ 856 | /// │ HINT: │ 857 | /// │ Run `cargo tree -i mokio -e features` from both. │ 858 | /// └───────────────────────────────────────────────────────┘ 859 | /// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 860 | /// 861 | /// # Running compatibility checks manually 862 | /// 863 | /// Sometimes, you might want to check for ABI compatibility but you might not have 864 | /// any thread-local or process-local that `rubicon` could sneak a compatibility check into. 865 | /// 866 | /// In that case, you can run the compatibility check manually. Maybe you can sneak 867 | /// it into a `Deref` impl, something like that: 868 | /// 869 | /// ```rust,ignore 870 | /// rubicon::compatibility_check! { 871 | /// ("version", env!("CARGO_PKG_VERSION")), 872 | /// // etc. 873 | /// } 874 | /// 875 | /// pub struct MySuperWrapperType(String); 876 | /// 877 | /// impl Deref for MySuperWrapperType { 878 | /// type Target = MySuperType; 879 | /// 880 | /// fn deref(&self) -> &Self::Target { 881 | /// // this is a good time to run compatibility checks 882 | /// #[cfg(not(feature = "no-compatibility-checks-yolo"))] 883 | /// crate::compatibility_check_once(); 884 | /// 885 | /// &self.0 886 | /// } 887 | /// } 888 | /// ``` 889 | #[cfg(not(any(feature = "export-globals", feature = "import-globals")))] 890 | #[macro_export] 891 | macro_rules! compatibility_check { 892 | ($($feature:tt)*) => { 893 | pub fn compatibility_check_once() { 894 | // no-op unless we're importing/exporting globals 895 | } 896 | }; 897 | } 898 | -------------------------------------------------------------------------------- /test-crates/exports/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "exports" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "mokio", 10 | ] 11 | 12 | [[package]] 13 | name = "mokio" 14 | version = "0.1.0" 15 | dependencies = [ 16 | "rubicon", 17 | ] 18 | 19 | [[package]] 20 | name = "paste" 21 | version = "1.0.15" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 24 | 25 | [[package]] 26 | name = "rubicon" 27 | version = "2.0.0" 28 | dependencies = [ 29 | "paste", 30 | ] 31 | -------------------------------------------------------------------------------- /test-crates/exports/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "exports" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["dylib"] 9 | 10 | [dependencies] 11 | mokio = { version = "0.1.0", path = "../mokio" } 12 | rubicon = { path = "../../rubicon", features = ["export-globals"] } 13 | 14 | [features] 15 | mokio-timer = ["mokio/timer"] 16 | -------------------------------------------------------------------------------- /test-crates/exports/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use mokio; 2 | -------------------------------------------------------------------------------- /test-crates/mod_a/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "libc" 7 | version = "0.2.155" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 10 | 11 | [[package]] 12 | name = "mod_a" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "mokio", 16 | "rubicon", 17 | "soprintln", 18 | ] 19 | 20 | [[package]] 21 | name = "mokio" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "rubicon", 25 | ] 26 | 27 | [[package]] 28 | name = "paste" 29 | version = "1.0.15" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 32 | 33 | [[package]] 34 | name = "rubicon" 35 | version = "3.4.3" 36 | dependencies = [ 37 | "libc", 38 | "paste", 39 | "rustc_version", 40 | ] 41 | 42 | [[package]] 43 | name = "rustc_version" 44 | version = "0.4.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 47 | dependencies = [ 48 | "semver", 49 | ] 50 | 51 | [[package]] 52 | name = "semver" 53 | version = "1.0.23" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 56 | 57 | [[package]] 58 | name = "soprintln" 59 | version = "3.2.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "2cc96941d6cac2e2654f62398ae8319ff55a730d6ee2edc78d112f37e5507613" 62 | -------------------------------------------------------------------------------- /test-crates/mod_a/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mod_a" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | mokio = { version = "0.1.0", path = "../mokio" } 12 | rubicon = { path = "../../rubicon", features = ["import-globals"] } 13 | soprintln = { version = "3.0.0", features = ["print"] } 14 | -------------------------------------------------------------------------------- /test-crates/mod_a/src/lib.rs: -------------------------------------------------------------------------------- 1 | use soprintln::soprintln; 2 | use std::sync::atomic::Ordering; 3 | 4 | #[no_mangle] 5 | pub extern "Rust" fn init() { 6 | soprintln::init!(); 7 | mokio::MOKIO_TL1.with(|s| s.fetch_add(1, Ordering::Relaxed)); 8 | mokio::MOKIO_PL1.fetch_add(1, Ordering::Relaxed); 9 | 10 | let dangerous = mokio::inc_dangerous(); 11 | soprintln!("DANGEROUS is now {}", dangerous); 12 | } 13 | -------------------------------------------------------------------------------- /test-crates/mod_b/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "libc" 7 | version = "0.2.155" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 10 | 11 | [[package]] 12 | name = "mod_b" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "mokio", 16 | "rubicon", 17 | "soprintln", 18 | ] 19 | 20 | [[package]] 21 | name = "mokio" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "rubicon", 25 | ] 26 | 27 | [[package]] 28 | name = "paste" 29 | version = "1.0.15" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 32 | 33 | [[package]] 34 | name = "rubicon" 35 | version = "3.4.3" 36 | dependencies = [ 37 | "libc", 38 | "paste", 39 | "rustc_version", 40 | ] 41 | 42 | [[package]] 43 | name = "rustc_version" 44 | version = "0.4.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 47 | dependencies = [ 48 | "semver", 49 | ] 50 | 51 | [[package]] 52 | name = "semver" 53 | version = "1.0.23" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 56 | 57 | [[package]] 58 | name = "soprintln" 59 | version = "3.2.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "2cc96941d6cac2e2654f62398ae8319ff55a730d6ee2edc78d112f37e5507613" 62 | -------------------------------------------------------------------------------- /test-crates/mod_b/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mod_b" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | mokio = { version = "0.1.0", path = "../mokio" } 12 | rubicon = { path = "../../rubicon", features = ["import-globals"] } 13 | soprintln = { version = "3.0.0", features = ["print"] } 14 | -------------------------------------------------------------------------------- /test-crates/mod_b/src/lib.rs: -------------------------------------------------------------------------------- 1 | use soprintln::soprintln; 2 | use std::sync::atomic::Ordering; 3 | 4 | #[no_mangle] 5 | pub extern "Rust" fn init() { 6 | soprintln::init!(); 7 | mokio::MOKIO_TL1.with(|s| s.fetch_add(1, Ordering::Relaxed)); 8 | mokio::MOKIO_PL1.fetch_add(1, Ordering::Relaxed); 9 | 10 | let dangerous = mokio::inc_dangerous(); 11 | soprintln!("DANGEROUS is now {}", dangerous); 12 | } 13 | -------------------------------------------------------------------------------- /test-crates/mokio/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ctor" 7 | version = "0.2.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" 10 | dependencies = [ 11 | "quote", 12 | "syn", 13 | ] 14 | 15 | [[package]] 16 | name = "mokio" 17 | version = "0.1.0" 18 | dependencies = [ 19 | "rubicon", 20 | ] 21 | 22 | [[package]] 23 | name = "paste" 24 | version = "1.0.15" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 27 | 28 | [[package]] 29 | name = "proc-macro2" 30 | version = "1.0.86" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 33 | dependencies = [ 34 | "unicode-ident", 35 | ] 36 | 37 | [[package]] 38 | name = "quote" 39 | version = "1.0.36" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 42 | dependencies = [ 43 | "proc-macro2", 44 | ] 45 | 46 | [[package]] 47 | name = "rubicon" 48 | version = "3.0.1" 49 | dependencies = [ 50 | "ctor", 51 | "paste", 52 | "rustc_version", 53 | ] 54 | 55 | [[package]] 56 | name = "rustc_version" 57 | version = "0.4.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 60 | dependencies = [ 61 | "semver", 62 | ] 63 | 64 | [[package]] 65 | name = "semver" 66 | version = "1.0.23" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 69 | 70 | [[package]] 71 | name = "syn" 72 | version = "2.0.72" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" 75 | dependencies = [ 76 | "proc-macro2", 77 | "quote", 78 | "unicode-ident", 79 | ] 80 | 81 | [[package]] 82 | name = "unicode-ident" 83 | version = "1.0.12" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 86 | -------------------------------------------------------------------------------- /test-crates/mokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mokio" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | rubicon = { path = "../../rubicon" } 9 | 10 | [features] 11 | default = [] 12 | timer = [] 13 | -------------------------------------------------------------------------------- /test-crates/mokio/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::AtomicU64, Arc, Mutex}; 2 | 3 | rubicon::compatibility_check! { 4 | ("mokio_pkg_version", env!("CARGO_PKG_VERSION")), 5 | 6 | #[cfg(not(feature = "timer"))] 7 | ("timer", "disabled"), 8 | #[cfg(feature = "timer")] 9 | ("timer", "enabled"), 10 | 11 | #[cfg(not(feature = "timer"))] 12 | ("timer_is_disabled", "1"), 13 | } 14 | 15 | #[derive(Default)] 16 | #[cfg(feature = "timer")] 17 | struct TimerInternals { 18 | #[allow(dead_code)] 19 | random_stuff: [u64; 4], 20 | } 21 | 22 | #[derive(Default)] 23 | pub struct Runtime { 24 | #[cfg(feature = "timer")] 25 | #[allow(dead_code)] 26 | timer: TimerInternals, 27 | 28 | // this field is second on purpose so that it'll be offset 29 | // if the feature is enabled/disabled 30 | pub counter: u64, 31 | } 32 | 33 | rubicon::process_local! { 34 | pub static MOKIO_PL1: AtomicU64 = AtomicU64::new(0); 35 | pub static MOKIO_PL2: AtomicU64 = AtomicU64::new(0); 36 | 37 | pub static mut DANGEROUS: u64 = 0; 38 | static DANGEROUS_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); 39 | } 40 | 41 | rubicon::thread_local! { 42 | pub static MOKIO_TL1: AtomicU64 = AtomicU64::new(0); 43 | pub static MOKIO_TL2: Arc> = Arc::new(Mutex::new(Runtime::default())); 44 | } 45 | 46 | pub fn inc_dangerous() -> u64 { 47 | let _guard = DANGEROUS_MUTEX.lock().unwrap(); 48 | unsafe { 49 | DANGEROUS += 1; 50 | DANGEROUS 51 | } 52 | } 53 | 54 | pub fn get_dangerous() -> u64 { 55 | let _guard = DANGEROUS_MUTEX.lock().unwrap(); 56 | unsafe { DANGEROUS } 57 | } 58 | -------------------------------------------------------------------------------- /test-crates/samplebin/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "cfg-if" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 10 | 11 | [[package]] 12 | name = "exports" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "mokio", 16 | "rubicon", 17 | ] 18 | 19 | [[package]] 20 | name = "libloading" 21 | version = "0.8.5" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" 24 | dependencies = [ 25 | "cfg-if", 26 | "windows-targets", 27 | ] 28 | 29 | [[package]] 30 | name = "mokio" 31 | version = "0.1.0" 32 | dependencies = [ 33 | "rubicon", 34 | ] 35 | 36 | [[package]] 37 | name = "paste" 38 | version = "1.0.15" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 41 | 42 | [[package]] 43 | name = "rubicon" 44 | version = "3.4.3" 45 | dependencies = [ 46 | "paste", 47 | "rustc_version", 48 | ] 49 | 50 | [[package]] 51 | name = "rustc_version" 52 | version = "0.4.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 55 | dependencies = [ 56 | "semver", 57 | ] 58 | 59 | [[package]] 60 | name = "samplebin" 61 | version = "0.1.0" 62 | dependencies = [ 63 | "cfg-if", 64 | "exports", 65 | "libloading", 66 | "rubicon", 67 | "soprintln", 68 | ] 69 | 70 | [[package]] 71 | name = "semver" 72 | version = "1.0.23" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 75 | 76 | [[package]] 77 | name = "soprintln" 78 | version = "3.2.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "7561c6c12cc21c549193bb82f86800ee0d5d69e85a4393ee1a2766917c2a35cb" 81 | 82 | [[package]] 83 | name = "windows-targets" 84 | version = "0.52.6" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 87 | dependencies = [ 88 | "windows_aarch64_gnullvm", 89 | "windows_aarch64_msvc", 90 | "windows_i686_gnu", 91 | "windows_i686_gnullvm", 92 | "windows_i686_msvc", 93 | "windows_x86_64_gnu", 94 | "windows_x86_64_gnullvm", 95 | "windows_x86_64_msvc", 96 | ] 97 | 98 | [[package]] 99 | name = "windows_aarch64_gnullvm" 100 | version = "0.52.6" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 103 | 104 | [[package]] 105 | name = "windows_aarch64_msvc" 106 | version = "0.52.6" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 109 | 110 | [[package]] 111 | name = "windows_i686_gnu" 112 | version = "0.52.6" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 115 | 116 | [[package]] 117 | name = "windows_i686_gnullvm" 118 | version = "0.52.6" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 121 | 122 | [[package]] 123 | name = "windows_i686_msvc" 124 | version = "0.52.6" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 127 | 128 | [[package]] 129 | name = "windows_x86_64_gnu" 130 | version = "0.52.6" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 133 | 134 | [[package]] 135 | name = "windows_x86_64_gnullvm" 136 | version = "0.52.6" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 139 | 140 | [[package]] 141 | name = "windows_x86_64_msvc" 142 | version = "0.52.6" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 145 | -------------------------------------------------------------------------------- /test-crates/samplebin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "samplebin" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | cfg-if = "1.0.0" 9 | exports = { version = "0.1.0", path = "../exports" } 10 | libloading = "0.8.4" 11 | rubicon = { path = "../../rubicon" } 12 | soprintln = { version = "3.0.0", features = ["print"] } 13 | -------------------------------------------------------------------------------- /test-crates/samplebin/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::Ordering; 2 | 3 | use exports::{self as _, mokio}; 4 | use soprintln::soprintln; 5 | 6 | fn main() { 7 | struct ModuleSpec { 8 | name: &'static str, 9 | channel: String, 10 | features: Vec, 11 | } 12 | 13 | let mut modules = [ 14 | ModuleSpec { 15 | name: "mod_a", 16 | channel: "stable".to_string(), 17 | features: Default::default(), 18 | }, 19 | ModuleSpec { 20 | name: "mod_b", 21 | channel: "stable".to_string(), 22 | features: Default::default(), 23 | }, 24 | ]; 25 | 26 | for arg in std::env::args().skip(1) { 27 | if let Some(rest) = arg.strip_prefix("--features:") { 28 | let parts: Vec<&str> = rest.splitn(2, '=').collect(); 29 | if parts.len() != 2 { 30 | panic!("Invalid argument format: expected --features:module=feature1,feature2"); 31 | } 32 | let mod_name = parts[0]; 33 | let features = parts[1].split(',').map(|s| s.to_owned()); 34 | let module = modules 35 | .iter_mut() 36 | .find(|m| m.name == mod_name) 37 | .unwrap_or_else(|| panic!("Unknown module: {}", mod_name)); 38 | 39 | for feature in features { 40 | module.features.push(feature); 41 | } 42 | } else if let Some(rest) = arg.strip_prefix("--channel:") { 43 | let parts: Vec<&str> = rest.splitn(2, '=').collect(); 44 | if parts.len() != 2 { 45 | panic!("Invalid argument format: expected --channel:module=(stable|nightly)"); 46 | } 47 | let mod_name = parts[0]; 48 | let channel = parts[1]; 49 | if channel != "stable" && channel != "nightly" { 50 | panic!( 51 | "Invalid channel: {}. Expected 'stable' or 'nightly'", 52 | channel 53 | ); 54 | } 55 | let module = modules 56 | .iter_mut() 57 | .find(|m| m.name == mod_name) 58 | .unwrap_or_else(|| panic!("Unknown module: {}", mod_name)); 59 | module.channel = channel.to_string(); 60 | } else { 61 | panic!("Unknown argument: {}", arg); 62 | } 63 | } 64 | 65 | soprintln::init!(); 66 | let exe_path = std::env::current_exe().expect("Failed to get current exe path"); 67 | let project_root = exe_path 68 | .parent() 69 | .unwrap() 70 | .parent() 71 | .unwrap() 72 | .parent() 73 | .unwrap(); 74 | std::env::set_current_dir(project_root).expect("Failed to change directory"); 75 | 76 | soprintln!("app starting up..."); 77 | 78 | for module in modules { 79 | cfg_if::cfg_if! { 80 | if #[cfg(target_os = "macos")] { 81 | let rustflags = "-Clink-arg=-undefined -Clink-arg=dynamic_lookup"; 82 | } else if #[cfg(target_os = "windows")] { 83 | let rustflags = "-Clink-arg=/FORCE:UNRESOLVED"; 84 | } else { 85 | let rustflags = ""; 86 | } 87 | } 88 | 89 | let mut cmd = std::process::Command::new("cargo"); 90 | cmd.arg(format!("+{}", module.channel)) 91 | .arg("build") 92 | .env("RUSTFLAGS", rustflags) 93 | .current_dir(format!("../{}", module.name)); 94 | if !module.features.is_empty() { 95 | cmd.arg("--features").arg(module.features.join(",")); 96 | } 97 | 98 | let output = cmd.output().expect("Failed to execute cargo build"); 99 | 100 | if !output.status.success() { 101 | eprintln!( 102 | "Error building {}: {}", 103 | module.name, 104 | String::from_utf8_lossy(&output.stderr) 105 | ); 106 | std::process::exit(1); 107 | } 108 | } 109 | 110 | fn module_path(name: &str) -> String { 111 | #[cfg(target_os = "windows")] 112 | let prefix = ""; 113 | #[cfg(not(target_os = "windows"))] 114 | let prefix = "lib"; 115 | 116 | #[cfg(target_os = "windows")] 117 | let extension = "dll"; 118 | #[cfg(target_os = "macos")] 119 | let extension = "dylib"; 120 | #[cfg(target_os = "linux")] 121 | let extension = "so"; 122 | 123 | format!( 124 | "../mod_{}/target/debug/{}mod_{}.{}", 125 | name, prefix, name, extension 126 | ) 127 | } 128 | 129 | soprintln!("loading modules..."); 130 | let lib_a = unsafe { libloading::Library::new(module_path("a")).unwrap() }; 131 | let lib_a = Box::leak(Box::new(lib_a)); 132 | let init_a: libloading::Symbol = unsafe { lib_a.get(b"init").unwrap() }; 133 | let init_a = Box::leak(Box::new(init_a)); 134 | 135 | let lib_b = unsafe { libloading::Library::new(module_path("b")).unwrap() }; 136 | let lib_b = Box::leak(Box::new(lib_b)); 137 | let init_b: libloading::Symbol = unsafe { lib_b.get(b"init").unwrap() }; 138 | let init_b = Box::leak(Box::new(init_b)); 139 | 140 | soprintln!( 141 | "PL1 = {}, TL1 = {} (initial)", 142 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 143 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 144 | ); 145 | 146 | for _ in 0..2 { 147 | unsafe { init_a() }; 148 | soprintln!( 149 | "PL1 = {}, TL1 = {} (after init_a)", 150 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 151 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 152 | ); 153 | 154 | unsafe { init_b() }; 155 | soprintln!( 156 | "PL1 = {}, TL1 = {} (after init_b)", 157 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 158 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 159 | ); 160 | } 161 | 162 | soprintln!("now starting a couple threads"); 163 | 164 | let mut join_handles = vec![]; 165 | for id in 1..=3 { 166 | let init_a = &*init_a; 167 | let init_b = &*init_b; 168 | 169 | let thread_name = format!("worker-{}", id); 170 | let jh = std::thread::Builder::new() 171 | .name(thread_name.clone()) 172 | .spawn(move || { 173 | soprintln!("in a separate thread named: {}", thread_name); 174 | 175 | soprintln!( 176 | "PL1 = {}, TL1 = {} (initial)", 177 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 178 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 179 | ); 180 | 181 | for _ in 0..2 { 182 | unsafe { init_a() }; 183 | soprintln!( 184 | "PL1 = {}, TL1 = {} (after init_a)", 185 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 186 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 187 | ); 188 | 189 | unsafe { init_b() }; 190 | soprintln!( 191 | "PL1 = {}, TL1 = {} (after init_b)", 192 | mokio::MOKIO_PL1.load(Ordering::Relaxed), 193 | mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 194 | ); 195 | } 196 | 197 | // TL1 should be 4 (incremented by each `init_X()` call) 198 | assert_eq!(mokio::MOKIO_TL1.with(|s| s.load(Ordering::Relaxed)), 4); 199 | 200 | id 201 | }) 202 | .unwrap(); 203 | join_handles.push(jh); 204 | } 205 | 206 | // join all the threads 207 | for jh in join_handles { 208 | let id = jh.join().unwrap(); 209 | soprintln!("thread {} joined", id); 210 | } 211 | 212 | // PL1 should be exactly 16 213 | // 2 per turn, 2 turns on the main thread, 2 turns on each of the 3 worker threads: 16 total 214 | assert_eq!(mokio::MOKIO_PL1.load(Ordering::Relaxed), 16); 215 | 216 | // same for DANGEROUS, it's just guarded by a mutex internally 217 | assert_eq!(mokio::get_dangerous(), 16); 218 | } 219 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target 3 | -------------------------------------------------------------------------------- /tests/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "tests" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | -------------------------------------------------------------------------------- /tests/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tests", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "chalk": "^5.3.0" 13 | } 14 | }, 15 | "node_modules/chalk": { 16 | "version": "5.3.0", 17 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", 18 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 19 | "license": "MIT", 20 | "engines": { 21 | "node": "^12.17.0 || ^14.13 || >=16.0.0" 22 | }, 23 | "funding": { 24 | "url": "https://github.com/chalk/chalk?sponsor=1" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::io; 3 | use std::path::Path; 4 | use std::process::{Command, Stdio}; 5 | 6 | #[derive(Clone, Default)] 7 | struct EnvVars { 8 | library_search_paths: Vec, 9 | } 10 | 11 | impl EnvVars { 12 | fn new() -> Self { 13 | EnvVars { 14 | library_search_paths: Vec::new(), 15 | } 16 | } 17 | 18 | fn add_library_path(&mut self, path: String) { 19 | self.library_search_paths.push(path); 20 | } 21 | 22 | fn each_kv(&self, mut f: F) 23 | where 24 | F: FnMut(&str, &str), 25 | { 26 | let platform = env::consts::OS; 27 | let (env_var, separator) = match platform { 28 | "macos" => ("DYLD_LIBRARY_PATH", ":"), 29 | "windows" => ("PATH", ";"), 30 | "linux" => ("LD_LIBRARY_PATH", ":"), 31 | _ => { 32 | eprintln!("❌ Unsupported platform: {}", platform); 33 | std::process::exit(1); 34 | } 35 | }; 36 | 37 | let value = self.library_search_paths.join(separator); 38 | f(env_var, &value); 39 | } 40 | 41 | fn with_additional_library_path(&self, path: String) -> Self { 42 | let mut new_env_vars = self.clone(); 43 | new_env_vars.add_library_path(path); 44 | new_env_vars 45 | } 46 | } 47 | 48 | fn set_env_variables() -> EnvVars { 49 | let mut env_vars = EnvVars::new(); 50 | 51 | let rust_sysroot = Command::new("rustc") 52 | .arg("--print") 53 | .arg("sysroot") 54 | .output() 55 | .expect("Failed to execute rustc") 56 | .stdout; 57 | let rust_sysroot = String::from_utf8_lossy(&rust_sysroot).trim().to_string(); 58 | 59 | let rust_nightly_sysroot = Command::new("rustc") 60 | .args(["+nightly", "--print", "sysroot"]) 61 | .output() 62 | .expect("Failed to execute rustc +nightly") 63 | .stdout; 64 | let rust_nightly_sysroot = String::from_utf8_lossy(&rust_nightly_sysroot) 65 | .trim() 66 | .to_string(); 67 | 68 | let platform = env::consts::OS; 69 | 70 | env_vars.add_library_path(format!("{}/lib", rust_sysroot)); 71 | env_vars.add_library_path(format!("{}/lib", rust_nightly_sysroot)); 72 | 73 | match platform { 74 | "macos" | "linux" => { 75 | // okay 76 | } 77 | "windows" => { 78 | let current_path = env::var("PATH").unwrap_or_default(); 79 | env_vars.add_library_path(current_path); 80 | } 81 | _ => { 82 | eprintln!("❌ Unsupported platform: {}", platform); 83 | std::process::exit(1); 84 | } 85 | } 86 | 87 | println!("\nEnvironment Variables Summary:"); 88 | env_vars.each_kv(|key, value| { 89 | println!("{}: {}", key, value); 90 | }); 91 | 92 | env_vars 93 | } 94 | 95 | fn run_command(command: &[&str], env_vars: &EnvVars) -> io::Result<(bool, String)> { 96 | use std::io::{BufRead, BufReader}; 97 | use std::sync::mpsc; 98 | use std::thread; 99 | 100 | let program = command[0]; 101 | let args = &command[1..]; 102 | 103 | println!("Running command: {} {:?}", program, args); 104 | 105 | let mut command = Command::new(program); 106 | command 107 | .args(args) 108 | .stdin(Stdio::inherit()) 109 | .stdout(Stdio::piped()) 110 | .stderr(Stdio::piped()); 111 | 112 | env_vars.each_kv(|key, value| { 113 | command.env(key, value); 114 | }); 115 | 116 | let mut child = command.spawn()?; 117 | 118 | let (tx_stdout, rx_stdout) = mpsc::channel(); 119 | let (tx_stderr, rx_stderr) = mpsc::channel(); 120 | 121 | let stdout = child.stdout.take().expect("Failed to capture stdout"); 122 | let stderr = child.stderr.take().expect("Failed to capture stderr"); 123 | 124 | let stdout_thread = thread::spawn(move || { 125 | let reader = BufReader::new(stdout); 126 | for line in reader.lines() { 127 | let line = line.expect("Failed to read line from stdout"); 128 | println!("{}", line); 129 | tx_stdout.send(line).expect("Failed to send stdout line"); 130 | } 131 | }); 132 | 133 | let stderr_thread = thread::spawn(move || { 134 | let reader = BufReader::new(stderr); 135 | for line in reader.lines() { 136 | let line = line.expect("Failed to read line from stderr"); 137 | eprintln!("{}", line); 138 | tx_stderr.send(line).expect("Failed to send stderr line"); 139 | } 140 | }); 141 | 142 | let mut output = String::new(); 143 | 144 | for line in rx_stdout.iter() { 145 | output.push_str(&line); 146 | output.push('\n'); 147 | } 148 | 149 | for line in rx_stderr.iter() { 150 | output.push_str(&line); 151 | output.push('\n'); 152 | } 153 | 154 | stdout_thread.join().expect("stdout thread panicked"); 155 | stderr_thread.join().expect("stderr thread panicked"); 156 | 157 | let status = child.wait()?; 158 | if !status.success() { 159 | if let Some(exit_code) = status.code() { 160 | eprintln!( 161 | "\n🔍 \x1b[1;90mProcess exited with code {} (0x{:X})\x1b[0m", 162 | exit_code, exit_code 163 | ); 164 | } else { 165 | #[cfg(unix)] 166 | { 167 | use std::os::unix::process::ExitStatusExt; 168 | if let Some(signal) = status.signal() { 169 | let signal_name = match signal { 170 | 1 => "SIGHUP", 171 | 2 => "SIGINT", 172 | 3 => "SIGQUIT", 173 | 4 => "SIGILL", 174 | 6 => "SIGABRT", 175 | 8 => "SIGFPE", 176 | 9 => "SIGKILL", 177 | 11 => "SIGSEGV", 178 | 13 => "SIGPIPE", 179 | 14 => "SIGALRM", 180 | 15 => "SIGTERM", 181 | _ => "Unknown", 182 | }; 183 | eprintln!( 184 | "\n🔍 \x1b[1;90mProcess terminated by signal {} ({})\x1b[0m", 185 | signal, signal_name 186 | ); 187 | } else { 188 | eprintln!("\n🔍 \x1b[1;90mProcess exited with unknown status\x1b[0m"); 189 | } 190 | } 191 | #[cfg(not(unix))] 192 | { 193 | eprintln!("\n🔍 \x1b[1;90mProcess exited with unknown status\x1b[0m"); 194 | } 195 | } 196 | } 197 | Ok((status.success(), output)) 198 | } 199 | 200 | fn check_feature_mismatch(output: &str) -> bool { 201 | output.contains("Feature mismatch for crate") 202 | } 203 | 204 | struct TestCase { 205 | name: &'static str, 206 | build_command: &'static [&'static str], 207 | run_command: &'static [&'static str], 208 | expected_result: &'static str, 209 | check_feature_mismatch: bool, 210 | allowed_to_fail: bool, 211 | } 212 | 213 | static TEST_CASES: &[TestCase] = &[ 214 | TestCase { 215 | name: "Tests pass (debug)", 216 | build_command: &[ 217 | "cargo", 218 | "build", 219 | "--manifest-path", 220 | "test-crates/samplebin/Cargo.toml", 221 | ], 222 | run_command: &["./test-crates/samplebin/target/debug/samplebin"], 223 | expected_result: "success", 224 | check_feature_mismatch: false, 225 | allowed_to_fail: false, 226 | }, 227 | TestCase { 228 | name: "Tests pass (release)", 229 | build_command: &[ 230 | "cargo", 231 | "build", 232 | "--manifest-path", 233 | "test-crates/samplebin/Cargo.toml", 234 | "--release", 235 | ], 236 | run_command: &["./test-crates/samplebin/target/release/samplebin"], 237 | expected_result: "success", 238 | check_feature_mismatch: false, 239 | allowed_to_fail: false, 240 | }, 241 | TestCase { 242 | name: "Bin stable, mod_a nightly (should fail)", 243 | build_command: &[ 244 | "cargo", 245 | "+stable", 246 | "build", 247 | "--manifest-path", 248 | "test-crates/samplebin/Cargo.toml", 249 | ], 250 | run_command: &[ 251 | "./test-crates/samplebin/target/debug/samplebin", 252 | "--channel:mod_a=nightly", 253 | ], 254 | expected_result: "fail", 255 | check_feature_mismatch: true, 256 | allowed_to_fail: cfg!(target_os = "linux"), 257 | }, 258 | TestCase { 259 | name: "Bin nightly, mod_a stable (should fail)", 260 | build_command: &[ 261 | "cargo", 262 | "+nightly", 263 | "build", 264 | "--manifest-path", 265 | "test-crates/samplebin/Cargo.toml", 266 | ], 267 | run_command: &[ 268 | "./test-crates/samplebin/target/debug/samplebin", 269 | "--channel:mod_a=stable", 270 | ], 271 | expected_result: "fail", 272 | check_feature_mismatch: true, 273 | allowed_to_fail: cfg!(target_os = "linux"), 274 | }, 275 | TestCase { 276 | name: "All nightly (should work)", 277 | build_command: &[ 278 | "cargo", 279 | "+nightly", 280 | "build", 281 | "--manifest-path", 282 | "test-crates/samplebin/Cargo.toml", 283 | ], 284 | run_command: &[ 285 | "./test-crates/samplebin/target/debug/samplebin", 286 | "--channel:mod_a=nightly", 287 | "--channel:mod_b=nightly", 288 | ], 289 | expected_result: "success", 290 | check_feature_mismatch: false, 291 | allowed_to_fail: false, 292 | }, 293 | TestCase { 294 | name: "Bin has mokio-timer feature (should fail)", 295 | build_command: &[ 296 | "cargo", 297 | "build", 298 | "--features=exports/mokio-timer", 299 | "--manifest-path", 300 | "test-crates/samplebin/Cargo.toml", 301 | ], 302 | run_command: &["./test-crates/samplebin/target/debug/samplebin"], 303 | expected_result: "fail", 304 | check_feature_mismatch: true, 305 | allowed_to_fail: false, 306 | }, 307 | TestCase { 308 | name: "mod_a has mokio-timer feature (should fail)", 309 | build_command: &[ 310 | "cargo", 311 | "build", 312 | "--manifest-path", 313 | "test-crates/samplebin/Cargo.toml", 314 | ], 315 | run_command: &[ 316 | "./test-crates/samplebin/target/debug/samplebin", 317 | "--features:mod_a=mokio/timer", 318 | ], 319 | expected_result: "fail", 320 | check_feature_mismatch: true, 321 | allowed_to_fail: false, 322 | }, 323 | TestCase { 324 | name: "mod_b has mokio-timer feature (should fail)", 325 | build_command: &[ 326 | "cargo", 327 | "build", 328 | "--manifest-path", 329 | "test-crates/samplebin/Cargo.toml", 330 | ], 331 | run_command: &[ 332 | "./test-crates/samplebin/target/debug/samplebin", 333 | "--features:mod_b=mokio/timer", 334 | ], 335 | expected_result: "fail", 336 | check_feature_mismatch: true, 337 | allowed_to_fail: false, 338 | }, 339 | TestCase { 340 | name: "all mods have mokio-timer feature (should fail)", 341 | build_command: &[ 342 | "cargo", 343 | "build", 344 | "--manifest-path", 345 | "test-crates/samplebin/Cargo.toml", 346 | ], 347 | run_command: &[ 348 | "./test-crates/samplebin/target/debug/samplebin", 349 | "--features:mod_a=mokio/timer", 350 | "--features:mod_b=mokio/timer", 351 | ], 352 | expected_result: "fail", 353 | check_feature_mismatch: true, 354 | allowed_to_fail: false, 355 | }, 356 | TestCase { 357 | name: "bin and mods have mokio-timer feature (should work)", 358 | build_command: &[ 359 | "cargo", 360 | "build", 361 | "--features=exports/mokio-timer", 362 | "--manifest-path", 363 | "test-crates/samplebin/Cargo.toml", 364 | ], 365 | run_command: &[ 366 | "./test-crates/samplebin/target/debug/samplebin", 367 | "--features:mod_a=mokio/timer", 368 | "--features:mod_b=mokio/timer", 369 | ], 370 | expected_result: "success", 371 | check_feature_mismatch: false, 372 | allowed_to_fail: false, 373 | }, 374 | ]; 375 | 376 | fn run_tests() -> io::Result<()> { 377 | println!("\n🚀 \x1b[1;36mChanging working directory to Git root...\x1b[0m"); 378 | let mut git_root = env::current_dir()?; 379 | 380 | while !Path::new(&git_root).join(".git").exists() { 381 | if let Some(parent) = git_root.parent() { 382 | git_root = parent.to_path_buf(); 383 | } else { 384 | eprintln!("❌ \x1b[1;31mGit root not found. Exiting.\x1b[0m"); 385 | std::process::exit(1); 386 | } 387 | } 388 | 389 | env::set_current_dir(&git_root)?; 390 | println!( 391 | "📂 \x1b[1;32mChanged working directory to:\x1b[0m {}", 392 | git_root.display() 393 | ); 394 | 395 | println!("🌟 \x1b[1;36mSetting up environment variables...\x1b[0m"); 396 | let env_vars = set_env_variables(); 397 | 398 | println!("🌙 \x1b[1;34mInstalling nightly Rust...\x1b[0m"); 399 | run_command(&["rustup", "toolchain", "add", "nightly"], &env_vars)?; 400 | 401 | println!("\n🧪 \x1b[1;35mRunning tests...\x1b[0m"); 402 | 403 | for (index, test) in TEST_CASES.iter().enumerate() { 404 | { 405 | let test_info = format!("Running test {}: {}", index + 1, test.name); 406 | let box_width = test_info.chars().count() + 4; 407 | let padding = box_width - 2 - test_info.chars().count(); 408 | let left_padding = padding / 2; 409 | let right_padding = padding - left_padding; 410 | 411 | println!("\n\x1b[1;33m╔{}╗\x1b[0m", "═".repeat(box_width - 2)); 412 | println!( 413 | "\x1b[1;33m║\x1b[0m{}\x1b[1;36m{}\x1b[0m{}\x1b[1;33m║\x1b[0m", 414 | " ".repeat(left_padding), 415 | test_info, 416 | " ".repeat(right_padding), 417 | ); 418 | println!("\x1b[1;33m╚{}╝\x1b[0m", "═".repeat(box_width - 2)); 419 | } 420 | 421 | println!("🏗️ \x1b[1;34mBuilding...\x1b[0m"); 422 | let build_result = run_command(test.build_command, &Default::default())?; 423 | if !build_result.0 { 424 | eprintln!("❌ \x1b[1;31mBuild failed. Exiting tests.\x1b[0m"); 425 | std::process::exit(1); 426 | } 427 | 428 | println!("▶️ \x1b[1;32mRunning...\x1b[0m"); 429 | let profile = if test.build_command.contains(&"--release") { 430 | "release" 431 | } else { 432 | "debug" 433 | }; 434 | let additional_path = git_root 435 | .join("test-crates") 436 | .join("samplebin") 437 | .join("target") 438 | .join(profile); 439 | let env_vars = 440 | env_vars.with_additional_library_path(additional_path.to_string_lossy().into_owned()); 441 | 442 | let (success, output) = run_command(test.run_command, &env_vars)?; 443 | 444 | match (test.expected_result, success) { 445 | ("success", true) => println!("✅ \x1b[1;32mTest passed as expected.\x1b[0m"), 446 | ("fail", false) if test.check_feature_mismatch && check_feature_mismatch(&output) => { 447 | println!("✅ \x1b[1;33mTest failed with feature mismatch as expected.\x1b[0m") 448 | } 449 | ("fail", false) if test.check_feature_mismatch => { 450 | eprintln!("❌ \x1b[1;31mTest failed, but not with the expected feature mismatch error.\x1b[0m"); 451 | if test.allowed_to_fail || cfg!(windows) { 452 | println!("⚠️ \x1b[1;33mTest was allowed to fail.\x1b[0m"); 453 | } else { 454 | std::process::exit(1); 455 | } 456 | } 457 | _ => { 458 | eprintln!( 459 | "❌ \x1b[1;31mTest result unexpected. Expected {}, but got {}.\x1b[0m", 460 | test.expected_result, 461 | if success { "success" } else { "failure" } 462 | ); 463 | if test.allowed_to_fail { 464 | println!("⚠️ \x1b[1;33mTest was allowed to fail.\x1b[0m"); 465 | } else { 466 | std::process::exit(1); 467 | } 468 | } 469 | } 470 | } 471 | 472 | println!("\n🎉 \x1b[1;32mAll tests passed successfully.\x1b[0m"); 473 | Ok(()) 474 | } 475 | 476 | fn main() -> io::Result<()> { 477 | run_tests() 478 | } 479 | --------------------------------------------------------------------------------