| author | bors <bors@rust-lang.org> 2026-06-03 16:26:56 UTC |
| committer | bors <bors@rust-lang.org> 2026-06-03 16:26:56 UTC |
| log | 9c963eecaaa5e9ef270e235a8b35f05e33b597ed |
| tree | 551617384479b62c6a484c37640c26b19d6ba4c4 |
| parent | e16420030e6020d6a38db26d3ed5911a8818b550 |
| parent | 336dc57c296f3d999d28b797aaf0333cf152e75e |
Switch the destructors implementation for thread locals on Windows to use FLS
## Summary
Switch the thread local **destructors** implementation on Windows to use the _Fiber Local Storage_ APIs, which provide native support for setting a callback to be called on thread termination, replacing the current `tls_callback` symbol-based implementation.
_Except for some spellchecking, no LLMs were used to produce code / comments / text in this PR._
## Current Implementation
On Windows, in order to support thread locals with destructors,
the standard library uses a special `tls_callback` symbol that is used to call the `destructors::run()` hook on thread termination.
This has two downsides:
1. It is not well documented, and seems to cause some problems [1] [2] [3].
2. It disallows some synchronization operations, as mentioned in [`LocalKey`'s documentation](https://doc.rust-lang.org/std/thread/struct.LocalKey.html#synchronization-in-thread-local-destructors).
[1]: https://github.com/rust-lang/rust/pull/144234
[2]: https://github.com/rust-lang/rust/issues/145154
[3]: https://github.com/rust-lang/rust/issues/140798
as an example of point 2, this code, which uses `JoinHandle::join` in a thread local Drop impl, will deadlock on stable:
<details>
<summary>Join-on-Drop Deadlock Example</summary>
```rust
struct JoinOnDrop(Option<JoinHandle<()>>);
impl Drop for JoinOnDrop {
fn drop(&mut self) {
self.0.take().unwrap().join().unwrap();
}
}
thread_local! {
static HANDLE: JoinOnDrop = {
let thread = std::thread::spawn(|| {
println!("Starting...");
// std::thread::sleep(Duration::from_secs(3));
println!("Done");
});
JoinOnDrop(Some(thread))
};
}
fn main() {
let thread = std::thread::spawn(|| {
HANDLE.with(|_| {
println!("Some other thread");
})
});
thread.join().unwrap();
println!("Done");
}
```
</details>
## Proposed Change
We can use the `Fls{Alloc,Set,Get,Free}` functions (see https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989)
to implement the dtor callback needed for thread locals that have a Drop implementation.
We allocate a single key, and use its destructor callback to run all the registered destructors when a thread is shutting down.
With this implementation, the above code sample will not deadlock (but it still might not be a good idea to do this!).
## Safety and Compatibility
We use the common `thread_local` + atomic pattern to only set a single FLS key.
The destructor callback is only called when that value is non-zero.
Destructors will only run at thread exit: we verify that we are not running in a fiber during the destructors callback. **This means that using fibers (which is very rare) will result in thread locals being leaked**, unless the fiber is converted back to a thread using `ConvertFiberToThread` before thread termination. This is not ideal, but should be OK as destructors are not guaranteed to run, but it needs to be documented.
It might be possible for the user to use something like the current `tls_callback` to observe an already-freed thread locals, which is something that can also happen in the current implementation.
~Destructors will only run on the correct thread: Fibers cannot be moved between threads.~
Destructors will only run on the correct thread: the hook uses a `#[thread_local]` list, so fiber movement between threads does not change which which thread executes the destructors.
Destructors will only run once: even if the hook is called multiple time, the `#[thread_local]` list is cleared after the first run.
Users cannot observe different locals because they are using fibers: because we only use an Fls local marker to trigger the destructors callback, we don't change anything about how users interact with "normal" thread locals and fiber locals.
### DLL Unloading
It is possible to build a `cdylib` which uses thread locals and unload it dynamically using `FreeLibrary`. This can cause the OS to call into an unmapped cleanup hook, so we use `atexit` to manually free the special FLS key, which will also trigger the cleanup hook for each registered thread. This is safe because similar to thread shutdown, no user code can ran after this point, and only the destructors of the running thread will run.
see `tests/run-make/dynamic-loading-cdylib/load_and_unload.rs`.
## Other Notes
The implementation is based on the `key::racy` and `guard::apple` code, because we need a `LazyKey`-like racey static and an `enable` function.
While TLS slots are [limited to 1088](https://devblogs.microsoft.com/oldnewthing/20170712-00/?p=96585), FLS slots are currently [limited to 4000](https://devblogs.microsoft.com/windows-music-dev/effectively-removing-the-fls-slot-allocation-limit-in-windows-10/) per process.
### Miri
Because miri is aware to the thread local implementation, I also implemented these functions and support for them in the interpreter here:
https://github.com/rust-lang/miri/compare/master...ohadravid:miri:windows-fls-support?expand=1
I guess that this will need to be merged before this PR (if this is accepted) - let me know and I'll open that PR as well.
### Targets without `target_thread_local`
In `*-gnu` Windows targets, the `target_thread_local` feature is unavailable.
We could also change the "key" (non-`target_thread_local`) Windows impl at
`library\std\src\sys\thread_local\key\windows.rs`
to be based on the Fls functions. I can add it to this PR, or as a separate PR, if you think this is preferable.
`Cell` in a `#[thread_local]` is used to store the resulting key, like the other implementations. When `target_thread_local` isn't available, we always fetch the atomic and set the FLS key's value.15 files changed, 875 insertions(+), 113 deletions(-)
library/std/src/sys/pal/windows/c.rs+5-1| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | #![unstable(issue = "none", feature = "windows_c")] |
| 6 | 6 | #![allow(clippy::style)] |
| 7 | 7 | |
| 8 | use core::ffi::{CStr, c_uint, c_ulong, c_ushort, c_void}; | |
| 8 | use core::ffi::{CStr, c_int, c_uint, c_ulong, c_ushort, c_void}; | |
| 9 | 9 | use core::ptr; |
| 10 | 10 | |
| 11 | 11 | mod windows_sys; |
| ... | ... | @@ -241,3 +241,7 @@ cfg_select! { |
| 241 | 241 | // Only available starting with Windows 8. |
| 242 | 242 | #[cfg(not(target_vendor = "win7"))] |
| 243 | 243 | windows_link::link!("ws2_32.dll" "system" fn GetHostNameW(name : PWSTR, namelen : i32) -> i32); |
| 244 | ||
| 245 | unsafe extern "C" { | |
| 246 | pub fn atexit(cb: unsafe extern "C" fn()) -> c_int; | |
| 247 | } |
library/std/src/sys/pal/windows/c/bindings.txt+7| ... | ... | @@ -2130,6 +2130,11 @@ FindExSearchNameMatch |
| 2130 | 2130 | FindFirstFileExW |
| 2131 | 2131 | FindNextFileW |
| 2132 | 2132 | FIONBIO |
| 2133 | FLS_OUT_OF_INDEXES | |
| 2134 | FlsAlloc | |
| 2135 | FlsFree | |
| 2136 | FlsGetValue | |
| 2137 | FlsSetValue | |
| 2133 | 2138 | FlushFileBuffers |
| 2134 | 2139 | FORMAT_MESSAGE_ALLOCATE_BUFFER |
| 2135 | 2140 | FORMAT_MESSAGE_ARGUMENT_ARRAY |
| ... | ... | @@ -2258,6 +2263,7 @@ IPV6_DROP_MEMBERSHIP |
| 2258 | 2263 | IPV6_MREQ |
| 2259 | 2264 | IPV6_MULTICAST_LOOP |
| 2260 | 2265 | IPV6_V6ONLY |
| 2266 | IsThreadAFiber | |
| 2261 | 2267 | LINGER |
| 2262 | 2268 | listen |
| 2263 | 2269 | LocalFree |
| ... | ... | @@ -2318,6 +2324,7 @@ OPEN_ALWAYS |
| 2318 | 2324 | OPEN_EXISTING |
| 2319 | 2325 | OpenProcessToken |
| 2320 | 2326 | OVERLAPPED |
| 2327 | PFLS_CALLBACK_FUNCTION | |
| 2321 | 2328 | PIPE_ACCEPT_REMOTE_CLIENTS |
| 2322 | 2329 | PIPE_ACCESS_DUPLEX |
| 2323 | 2330 | PIPE_ACCESS_INBOUND |
library/std/src/sys/pal/windows/c/windows_sys.rs+8| ... | ... | @@ -27,6 +27,10 @@ windows_link::link!("kernel32.dll" "system" fn ExitProcess(uexitcode : u32) -> ! |
| 27 | 27 | windows_link::link!("kernel32.dll" "system" fn FindClose(hfindfile : HANDLE) -> BOOL); |
| 28 | 28 | windows_link::link!("kernel32.dll" "system" fn FindFirstFileExW(lpfilename : PCWSTR, finfolevelid : FINDEX_INFO_LEVELS, lpfindfiledata : *mut core::ffi::c_void, fsearchop : FINDEX_SEARCH_OPS, lpsearchfilter : *const core::ffi::c_void, dwadditionalflags : FIND_FIRST_EX_FLAGS) -> HANDLE); |
| 29 | 29 | windows_link::link!("kernel32.dll" "system" fn FindNextFileW(hfindfile : HANDLE, lpfindfiledata : *mut WIN32_FIND_DATAW) -> BOOL); |
| 30 | windows_link::link!("kernel32.dll" "system" fn FlsAlloc(lpcallback : PFLS_CALLBACK_FUNCTION) -> u32); | |
| 31 | windows_link::link!("kernel32.dll" "system" fn FlsFree(dwflsindex : u32) -> BOOL); | |
| 32 | windows_link::link!("kernel32.dll" "system" fn FlsGetValue(dwflsindex : u32) -> *mut core::ffi::c_void); | |
| 33 | windows_link::link!("kernel32.dll" "system" fn FlsSetValue(dwflsindex : u32, lpflsdata : *const core::ffi::c_void) -> BOOL); | |
| 30 | 34 | windows_link::link!("kernel32.dll" "system" fn FlushFileBuffers(hfile : HANDLE) -> BOOL); |
| 31 | 35 | windows_link::link!("kernel32.dll" "system" fn FormatMessageW(dwflags : FORMAT_MESSAGE_OPTIONS, lpsource : *const core::ffi::c_void, dwmessageid : u32, dwlanguageid : u32, lpbuffer : PWSTR, nsize : u32, arguments : *const *const i8) -> u32); |
| 32 | 36 | windows_link::link!("kernel32.dll" "system" fn FreeEnvironmentStringsW(penv : PCWSTR) -> BOOL); |
| ... | ... | @@ -67,6 +71,7 @@ windows_link::link!("kernel32.dll" "system" fn GetWindowsDirectoryW(lpbuffer : P |
| 67 | 71 | windows_link::link!("kernel32.dll" "system" fn InitOnceBeginInitialize(lpinitonce : *mut INIT_ONCE, dwflags : u32, fpending : *mut BOOL, lpcontext : *mut *mut core::ffi::c_void) -> BOOL); |
| 68 | 72 | windows_link::link!("kernel32.dll" "system" fn InitOnceComplete(lpinitonce : *mut INIT_ONCE, dwflags : u32, lpcontext : *const core::ffi::c_void) -> BOOL); |
| 69 | 73 | windows_link::link!("kernel32.dll" "system" fn InitializeProcThreadAttributeList(lpattributelist : LPPROC_THREAD_ATTRIBUTE_LIST, dwattributecount : u32, dwflags : u32, lpsize : *mut usize) -> BOOL); |
| 74 | windows_link::link!("kernel32.dll" "system" fn IsThreadAFiber() -> BOOL); | |
| 70 | 75 | windows_link::link!("kernel32.dll" "system" fn LocalFree(hmem : HLOCAL) -> HLOCAL); |
| 71 | 76 | windows_link::link!("kernel32.dll" "system" fn LockFileEx(hfile : HANDLE, dwflags : LOCK_FILE_FLAGS, dwreserved : u32, nnumberofbytestolocklow : u32, nnumberofbytestolockhigh : u32, lpoverlapped : *mut OVERLAPPED) -> BOOL); |
| 72 | 77 | windows_link::link!("kernel32.dll" "system" fn MoveFileExW(lpexistingfilename : PCWSTR, lpnewfilename : PCWSTR, dwflags : MOVE_FILE_FLAGS) -> BOOL); |
| ... | ... | @@ -2667,6 +2672,7 @@ impl Default for FLOATING_SAVE_AREA { |
| 2667 | 2672 | unsafe { core::mem::zeroed() } |
| 2668 | 2673 | } |
| 2669 | 2674 | } |
| 2675 | pub const FLS_OUT_OF_INDEXES: u32 = 4294967295u32; | |
| 2670 | 2676 | pub const FORMAT_MESSAGE_ALLOCATE_BUFFER: FORMAT_MESSAGE_OPTIONS = 256u32; |
| 2671 | 2677 | pub const FORMAT_MESSAGE_ARGUMENT_ARRAY: FORMAT_MESSAGE_OPTIONS = 8192u32; |
| 2672 | 2678 | pub const FORMAT_MESSAGE_FROM_HMODULE: FORMAT_MESSAGE_OPTIONS = 2048u32; |
| ... | ... | @@ -3037,6 +3043,8 @@ pub struct OVERLAPPED_0_0 { |
| 3037 | 3043 | } |
| 3038 | 3044 | pub type PCSTR = *const u8; |
| 3039 | 3045 | pub type PCWSTR = *const u16; |
| 3046 | pub type PFLS_CALLBACK_FUNCTION = | |
| 3047 | Option<unsafe extern "system" fn(lpflsdata: *const core::ffi::c_void)>; | |
| 3040 | 3048 | pub type PIO_APC_ROUTINE = Option< |
| 3041 | 3049 | unsafe extern "system" fn( |
| 3042 | 3050 | apccontext: *mut core::ffi::c_void, |
library/std/src/sys/thread_local/guard/windows.rs+200-85| ... | ... | @@ -1,103 +1,218 @@ |
| 1 | 1 | //! Support for Windows TLS destructors. |
| 2 | 2 | //! |
| 3 | //! Unfortunately, Windows does not provide a nice API to provide a destructor | |
| 4 | //! for a TLS variable. Thus, the solution here ended up being a little more | |
| 5 | //! obscure, but fear not, the internet has informed me [1][2] that this solution | |
| 6 | //! is not unique (no way I could have thought of it as well!). The key idea is | |
| 7 | //! to insert some hook somewhere to run arbitrary code on thread termination. | |
| 8 | //! With this in place we'll be able to run anything we like, including all | |
| 9 | //! TLS destructors! | |
| 3 | //! Windows has an API to provide a destructor for a FLS (fiber local storage) variable, | |
| 4 | //! which behaves similarly to a TLS variable for our purpose [1]. | |
| 10 | 5 | //! |
| 11 | //! In order to realize this, all TLS destructors are tracked by *us*, not the | |
| 12 | //! Windows runtime. This means that we have a global list of destructors for | |
| 6 | //! All TLS destructors are tracked by *us*, not the Windows runtime. | |
| 7 | //! This means that we have a global list of destructors for | |
| 13 | 8 | //! each TLS key or variable that we know about. |
| 14 | 9 | //! |
| 15 | //! # What's up with CRT$XLB? | |
| 16 | //! | |
| 17 | //! For anything about TLS destructors to work on Windows, we have to be able | |
| 18 | //! to run *something* when a thread exits. To do so, we place a very special | |
| 19 | //! static in a very special location. If this is encoded in just the right | |
| 20 | //! way, the kernel's loader is apparently nice enough to run some function | |
| 21 | //! of ours whenever a thread exits! How nice of the kernel! | |
| 22 | //! | |
| 23 | //! Lots of detailed information can be found in source [1] above, but the | |
| 24 | //! gist of it is that this is leveraging a feature of Microsoft's PE format | |
| 25 | //! (executable format) which is not actually used by any compilers today. | |
| 26 | //! This apparently translates to any callbacks in the ".CRT$XLB" section | |
| 27 | //! being run on certain events. | |
| 28 | //! | |
| 29 | //! So after all that, we use the compiler's `#[link_section]` feature to place | |
| 30 | //! a callback pointer into the magic section so it ends up being called. | |
| 31 | //! | |
| 32 | //! # What's up with this callback? | |
| 33 | //! | |
| 34 | //! The callback specified receives a number of parameters from... someone! | |
| 35 | //! (the kernel? the runtime? I'm not quite sure!) There are a few events that | |
| 36 | //! this gets invoked for, but we're currently only interested on when a | |
| 37 | //! thread or a process "detaches" (exits). The process part happens for the | |
| 38 | //! last thread and the thread part happens for any normal thread. | |
| 39 | //! | |
| 40 | //! # The article mentions weird stuff about "/INCLUDE"? | |
| 41 | //! | |
| 42 | //! It sure does! Specifically we're talking about this quote: | |
| 43 | //! | |
| 44 | //! ```quote | |
| 45 | //! The Microsoft run-time library facilitates this process by defining a | |
| 46 | //! memory image of the TLS Directory and giving it the special name | |
| 47 | //! “__tls_used” (Intel x86 platforms) or “_tls_used” (other platforms). The | |
| 48 | //! linker looks for this memory image and uses the data there to create the | |
| 49 | //! TLS Directory. Other compilers that support TLS and work with the | |
| 50 | //! Microsoft linker must use this same technique. | |
| 51 | //! ``` | |
| 52 | //! | |
| 53 | //! Basically what this means is that if we want support for our TLS | |
| 54 | //! destructors/our hook being called then we need to make sure the linker does | |
| 55 | //! not omit this symbol. Otherwise it will omit it and our callback won't be | |
| 56 | //! wired up. | |
| 57 | //! | |
| 58 | //! We don't actually use the `/INCLUDE` linker flag here like the article | |
| 59 | //! mentions because the Rust compiler doesn't propagate linker flags, but | |
| 60 | //! instead we use a shim function which performs a volatile 1-byte load from | |
| 61 | //! the address of the _tls_used symbol to ensure it sticks around. | |
| 62 | //! | |
| 63 | //! [1]: https://www.codeproject.com/Articles/8113/Thread-Local-Storage-The-C-Way | |
| 64 | //! [2]: https://github.com/ChromiumWebApps/chromium/blob/master/base/threading/thread_local_storage_win.cc#L42 | |
| 10 | //! [1]: https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=102989 | |
| 65 | 11 | |
| 66 | 12 | use core::ffi::c_void; |
| 13 | use core::sync::atomic::{AtomicBool, AtomicU32, Ordering, fence}; | |
| 67 | 14 | |
| 15 | use crate::cell::Cell; | |
| 68 | 16 | use crate::ptr; |
| 69 | use crate::sys::c; | |
| 17 | use crate::sys::c::{self, FLS_OUT_OF_INDEXES}; | |
| 18 | ||
| 19 | pub type Key = u32; | |
| 20 | ||
| 21 | unsafe fn create(dtor: c::PFLS_CALLBACK_FUNCTION) -> Key { | |
| 22 | let key_result = unsafe { c::FlsAlloc(dtor) }; | |
| 23 | ||
| 24 | if key_result == c::FLS_OUT_OF_INDEXES { | |
| 25 | rtabort!("out of FLS keys"); | |
| 26 | } | |
| 70 | 27 | |
| 71 | unsafe extern "C" { | |
| 72 | #[link_name = "_tls_used"] | |
| 73 | static TLS_USED: u8; | |
| 28 | key_result | |
| 74 | 29 | } |
| 75 | pub fn enable() { | |
| 76 | // When destructors are used, we need to add a reference to the _tls_used | |
| 77 | // symbol provided by the CRT, otherwise the TLS support code will get | |
| 78 | // GC'd by the linker and our callback won't be called. | |
| 79 | unsafe { ptr::from_ref(&TLS_USED).read_volatile() }; | |
| 80 | // We also need to reference CALLBACK to make sure it does not get GC'd | |
| 81 | // by the compiler/LLVM. The callback will end up inside the TLS | |
| 82 | // callback array pointed to by _TLS_USED through linker shenanigans, | |
| 83 | // but as far as the compiler is concerned, it looks like the data is | |
| 84 | // unused, so we need this hack to prevent it from disappearing. | |
| 85 | unsafe { ptr::from_ref(&CALLBACK).read_volatile() }; | |
| 30 | ||
| 31 | unsafe fn set(key: Key, ptr: *const c_void) { | |
| 32 | let result = unsafe { c::FlsSetValue(key, ptr) }; | |
| 33 | ||
| 34 | if result == c::FALSE { | |
| 35 | rtabort!("failed to set FLS value"); | |
| 36 | } | |
| 37 | } | |
| 38 | ||
| 39 | fn is_thread_a_fiber() -> bool { | |
| 40 | let res = unsafe { c::IsThreadAFiber() }; | |
| 41 | res == c::TRUE | |
| 42 | } | |
| 43 | ||
| 44 | static KEY: AtomicU32 = AtomicU32::new(FLS_OUT_OF_INDEXES); | |
| 45 | ||
| 46 | /// Used to track whether we are currently in the critical section of `enable`. | |
| 47 | /// For miri, these atomic operations cause synchronization that can mask user bugs, | |
| 48 | /// and they are not needed as `atexit` is anyway not supported, so we can skip them. | |
| 49 | struct EnableGuard; | |
| 50 | static AT_EXIT_HOOK_CALLED: AtomicBool = AtomicBool::new(false); | |
| 51 | static ACTIVE_ENABLE_CALLS: AtomicU32 = AtomicU32::new(0); | |
| 52 | ||
| 53 | impl EnableGuard { | |
| 54 | // Mark the start of an `enable` call, returning whether the `atexit` hook has already been called or not. | |
| 55 | fn new() -> (Self, bool) { | |
| 56 | if cfg!(miri) { | |
| 57 | return (Self, false); | |
| 58 | } | |
| 59 | ACTIVE_ENABLE_CALLS.fetch_add(1, Ordering::Relaxed); | |
| 60 | ||
| 61 | // Both `new` and `start_exit` publish state to one atomic and inspect the other. | |
| 62 | // `AcqRel` is insufficient because neither read is required to observe the other's publication, | |
| 63 | // so we could create the guard but `start_exit` would not see any active enable calls. | |
| 64 | // `SeqCst` ensures that there's a single global order between the publish and check, | |
| 65 | // so at least one side must observe the other and bail. | |
| 66 | fence(Ordering::SeqCst); | |
| 67 | ||
| 68 | let at_exit_called = AT_EXIT_HOOK_CALLED.load(Ordering::Relaxed); | |
| 69 | ||
| 70 | (Self, at_exit_called) | |
| 71 | } | |
| 72 | ||
| 73 | /// Mark the start of process exit, returning whether we should free the FLS key or not. | |
| 74 | fn start_exit() -> bool { | |
| 75 | // After this hook starts, new destructor registration will be skipped, | |
| 76 | // causing TLS destructors initialized after this point to leak. | |
| 77 | if AT_EXIT_HOOK_CALLED.swap(true, Ordering::Relaxed) { | |
| 78 | // Cleanup already started, there is nothing else to do. | |
| 79 | return false; | |
| 80 | } | |
| 81 | ||
| 82 | fence(Ordering::SeqCst); | |
| 83 | ||
| 84 | let any_active_enabled_called = ACTIVE_ENABLE_CALLS.load(Ordering::Relaxed) != 0; | |
| 85 | ||
| 86 | if any_active_enabled_called { | |
| 87 | // If another thread is currently in `enable`, it may already have loaded this key and may be about to call `FlsSetValue`. | |
| 88 | // So we must *not* call free the FLS key. | |
| 89 | // | |
| 90 | // During real process exit this is harmless because the `cleanup` hook is always available, | |
| 91 | // and the FLS callback will be triggered normally by the OS. | |
| 92 | // | |
| 93 | // During DLL unload, the unloader cannot safely have threads running code from the DLL except for the destructors, | |
| 94 | // so there must not be any `enable` calls active anyway. | |
| 95 | return false; | |
| 96 | } | |
| 97 | ||
| 98 | return true; | |
| 99 | } | |
| 86 | 100 | } |
| 87 | 101 | |
| 88 | #[unsafe(link_section = ".CRT$XLB")] | |
| 89 | #[cfg_attr(miri, used)] // Miri only considers explicitly `#[used]` statics for `lookup_link_section` | |
| 90 | pub static CALLBACK: unsafe extern "system" fn(*mut c_void, u32, *mut c_void) = tls_callback; | |
| 102 | #[cfg(not(miri))] | |
| 103 | impl Drop for EnableGuard { | |
| 104 | fn drop(&mut self) { | |
| 105 | ACTIVE_ENABLE_CALLS.fetch_sub(1, Ordering::Relaxed); | |
| 106 | } | |
| 107 | } | |
| 91 | 108 | |
| 92 | unsafe extern "system" fn tls_callback(_h: *mut c_void, dw_reason: u32, _pv: *mut c_void) { | |
| 93 | if dw_reason == c::DLL_THREAD_DETACH || dw_reason == c::DLL_PROCESS_DETACH { | |
| 94 | unsafe { | |
| 95 | #[cfg(target_thread_local)] | |
| 96 | super::super::destructors::run(); | |
| 97 | #[cfg(not(target_thread_local))] | |
| 98 | super::super::key::run_dtors(); | |
| 109 | pub fn enable() { | |
| 110 | let registered = if cfg!(target_thread_local) { | |
| 111 | #[thread_local] | |
| 112 | static REGISTERED: Cell<bool> = Cell::new(false); | |
| 113 | REGISTERED.replace(true) | |
| 114 | } else { | |
| 115 | // `#[thread_local]` is unavailable on windows-gnu (`target_thread_local` is off), | |
| 116 | // but setting the FLS key's value is about as expensive as `TlsGet`, so we don't bother tracking registration separately. | |
| 117 | false | |
| 118 | }; | |
| 99 | 119 | |
| 100 | crate::rt::thread_cleanup(); | |
| 120 | if !registered { | |
| 121 | // We are in a critical section where we are trying to register a destructor for the current thread. | |
| 122 | // We need to avoid racing with the `atexit` hook that frees the FLS slot, which would cause us to call `FlsSetValue` on a freed key, | |
| 123 | // or calling `atexit` during process shutdown, which would cause a deadlock. | |
| 124 | let (_guard, at_exit_called) = EnableGuard::new(); | |
| 125 | ||
| 126 | if at_exit_called { | |
| 127 | // We are exiting and don't want to race with the `atexit` hook, so we won't be able to run the destructors for this thread. | |
| 128 | return; | |
| 101 | 129 | } |
| 130 | ||
| 131 | let current_key = KEY.load(Ordering::Acquire); | |
| 132 | ||
| 133 | // If we already allocated a key, we only need to set it to a non-null value so that the destructors hook is run for this thread. | |
| 134 | let key = if current_key != FLS_OUT_OF_INDEXES { | |
| 135 | current_key | |
| 136 | } else { | |
| 137 | // Otherwise, we try to allocate a key. | |
| 138 | let new_key = unsafe { create(Some(cleanup)) }; | |
| 139 | ||
| 140 | // Now we need to set this key to be used by everyone else. | |
| 141 | // If we won the race, our key is the right one and we can set it to non-null value. | |
| 142 | // If we lost, we'll use the winning key and free our losing key. | |
| 143 | match KEY.compare_exchange(current_key, new_key, Ordering::Release, Ordering::Acquire) { | |
| 144 | Ok(_) => { | |
| 145 | // If the current DLL is unloaded, the registered `cleanup` hook will not be available later during thread exit, | |
| 146 | // triggering a `STATUS_ACCESS_VIOLATION`. To avoid this, we use the `atexit` hook, which is called during DLL unload | |
| 147 | // to manually free the FLS slot, triggering the destructors. | |
| 148 | // | |
| 149 | // However, calling `atexit` during process exit can cause a deadlock. | |
| 150 | // In a Rust binary, `enable` is called during the main thread startup and before any user code, | |
| 151 | // and we checked using `at_exit_called` that we aren't in process shutdown. | |
| 152 | // | |
| 153 | // In a Rust DLL, dynamic unloading can only happen safely when no other threads are | |
| 154 | // concurrently executing Rust code, so if we are here we cannot be unloading yet. | |
| 155 | // | |
| 156 | // If a main non-Rust binary is exiting, it must not be trigger the `enable` guard | |
| 157 | // for the first time during process shutdown. | |
| 158 | let res = unsafe { c::atexit(free_fls_key_at_exit) }; | |
| 159 | if res != 0 { | |
| 160 | rtabort!("failed to register fls atexit hook"); | |
| 161 | } | |
| 162 | ||
| 163 | new_key | |
| 164 | } | |
| 165 | Err(other_key) => { | |
| 166 | unsafe { c::FlsFree(new_key) }; | |
| 167 | other_key | |
| 168 | } | |
| 169 | } | |
| 170 | }; | |
| 171 | ||
| 172 | // Setting the key's value to non-zero will cause the dtor callback to be called when the thread exits. | |
| 173 | unsafe { set(key, ptr::without_provenance(1)) }; | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | extern "C" fn free_fls_key_at_exit() { | |
| 178 | // The main purpose of this hook is to free the FLS slot during DLL unload. | |
| 179 | // However, this hook will also be called during normal process exit, while other Rust threads are still running, | |
| 180 | // so we must be careful to avoid races with `enable`. | |
| 181 | let should_free_key = EnableGuard::start_exit(); | |
| 182 | if !should_free_key { | |
| 183 | return; | |
| 184 | } | |
| 185 | ||
| 186 | let current_key = KEY.swap(c::FLS_OUT_OF_INDEXES, Ordering::AcqRel); | |
| 187 | if current_key != c::FLS_OUT_OF_INDEXES { | |
| 188 | // Calling `FlsFree` will cause the OS to call the `cleanup` hook, in the current thread, *for each thread* (or fiber) with a value in this FLS slot. | |
| 189 | // `cleanup` is safe to run repeatedly: it only drains the current thread's TLS destructor list, and we check that we are not running in a fiber before doing so. | |
| 190 | // We only call this when no `enable` call is active, so it cannot race with `FlsSetValue` using this key. | |
| 191 | // Destructors of thread locals in other threads will not run and therefore leak, which is allowed since we are exiting or unloading. | |
| 192 | unsafe { c::FlsFree(current_key) }; | |
| 193 | } | |
| 194 | } | |
| 195 | ||
| 196 | unsafe extern "system" fn cleanup(_ptr: *const c_void) { | |
| 197 | // Avoid running the hook if we are in a fiber. | |
| 198 | // This will cause destructors of thread locals to not run, leaking them. | |
| 199 | // Thread-local runtime state will not be cleaned. | |
| 200 | // | |
| 201 | // We need to verify that we won't run the destructors *before* the thread exits, | |
| 202 | // but if the fiber that registered the callback is deleted, the thread might still be running other fibers. | |
| 203 | // | |
| 204 | // By checking that we are not running in a fiber here, we are guaranteed that the hook is only running during the thread's exit. | |
| 205 | // See also the `fiber_does_not_trigger_dtor` test. | |
| 206 | if is_thread_a_fiber() { | |
| 207 | return; | |
| 102 | 208 | } |
| 209 | ||
| 210 | unsafe { | |
| 211 | #[cfg(target_thread_local)] | |
| 212 | super::super::destructors::run(); | |
| 213 | #[cfg(not(target_thread_local))] | |
| 214 | super::super::key::run_dtors(); | |
| 215 | } | |
| 216 | ||
| 217 | crate::rt::thread_cleanup(); | |
| 103 | 218 | } |
library/std/src/thread/local.rs+9-8| ... | ... | @@ -98,17 +98,18 @@ use crate::fmt; |
| 98 | 98 | /// run on the thread that causes the process to exit. This is because the |
| 99 | 99 | /// other threads may be forcibly terminated. |
| 100 | 100 | /// |
| 101 | /// ## Synchronization in thread-local destructors | |
| 101 | /// If a thread is [converted into a fiber], destructors will not be run unless | |
| 102 | /// the fiber is [converted back into a thread] before the underlying thread exits. | |
| 102 | 103 | /// |
| 103 | /// On Windows, synchronization operations (such as [`JoinHandle::join`]) in | |
| 104 | /// thread local destructors are prone to deadlocks and so should be avoided. | |
| 105 | /// This is because the [loader lock] is held while a destructor is run. The | |
| 106 | /// lock is acquired whenever a thread starts or exits or when a DLL is loaded | |
| 107 | /// or unloaded. Therefore these events are blocked for as long as a thread | |
| 108 | /// local destructor is running. | |
| 104 | /// If a process loads a Rust `cdylib`, it must not cause the Rust TLS destructor support | |
| 105 | // to be initialized for the first time during process shutdown. | |
| 109 | 106 | /// |
| 107 | /// When dynamically unloading a Rust `cdylib`, threads with pending TLS destructors | |
| 108 | /// may run during the unload or may be leaked. | |
| 109 | /// | |
| 110 | /// [converted into a fiber]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-convertthreadtofiber | |
| 111 | /// [converted back into a thread]: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-convertfibertothread | |
| 110 | 112 | /// [loader lock]: https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices |
| 111 | /// [`JoinHandle::join`]: crate::thread::JoinHandle::join | |
| 112 | 113 | /// [`with`]: LocalKey::with |
| 113 | 114 | #[cfg_attr(not(test), rustc_diagnostic_item = "LocalKey")] |
| 114 | 115 | #[stable(feature = "rust1", since = "1.0.0")] |
library/std/tests/thread_local/tests.rs+55| ... | ... | @@ -21,6 +21,12 @@ impl Signal { |
| 21 | 21 | set = cvar.wait(set).unwrap(); |
| 22 | 22 | } |
| 23 | 23 | } |
| 24 | ||
| 25 | fn is_set(&self) -> bool { | |
| 26 | let (set, _cvar) = &*self.0; | |
| 27 | let set = set.lock().unwrap(); | |
| 28 | *set | |
| 29 | } | |
| 24 | 30 | } |
| 25 | 31 | |
| 26 | 32 | struct NotifyOnDrop(Signal); |
| ... | ... | @@ -97,6 +103,7 @@ fn smoke_dtor() { |
| 97 | 103 | }); |
| 98 | 104 | }); |
| 99 | 105 | signal.wait(); |
| 106 | assert!(signal.is_set()); | |
| 100 | 107 | t.join().unwrap(); |
| 101 | 108 | } |
| 102 | 109 | } |
| ... | ... | @@ -393,3 +400,51 @@ fn thread_current_in_dtor() { |
| 393 | 400 | let name = name.as_ref().unwrap(); |
| 394 | 401 | assert_eq!(name, "test"); |
| 395 | 402 | } |
| 403 | ||
| 404 | // Test that if a thread uses fibers while the dtors callback is running, | |
| 405 | // we do NOT trigger dtors. | |
| 406 | // This prevents a UAF if a fiber is the first to use Tls and is deleted, | |
| 407 | // like in the example here: | |
| 408 | // https://github.com/rust-lang/rust/pull/148799#issuecomment-3731806901 | |
| 409 | #[cfg(target_os = "windows")] | |
| 410 | #[test] | |
| 411 | fn fiber_does_not_trigger_dtor() { | |
| 412 | use core::ffi::c_void; | |
| 413 | use std::ptr; | |
| 414 | ||
| 415 | unsafe extern "system" { | |
| 416 | fn ConvertFiberToThread() -> i32; | |
| 417 | fn ConvertThreadToFiber(lpParameter: *const c_void) -> *mut c_void; | |
| 418 | } | |
| 419 | ||
| 420 | thread_local!(static FOO: UnsafeCell<Option<NotifyOnDrop>> = UnsafeCell::new(None)); | |
| 421 | let signal = Signal::default(); | |
| 422 | ||
| 423 | let signal2 = signal.clone(); | |
| 424 | let t = thread::spawn(move || unsafe { | |
| 425 | let mut signal = Some(signal2); | |
| 426 | FOO.with(|f| { | |
| 427 | *f.get() = Some(NotifyOnDrop(signal.take().unwrap())); | |
| 428 | }); | |
| 429 | let _main = ConvertThreadToFiber(ptr::null()); | |
| 430 | // A user can then switch to a new fiber and delete the main fiber. | |
| 431 | // If destructors are triggered when the fiber is deleted, | |
| 432 | // the new fiber will be able to observe already-destructed values. | |
| 433 | }); | |
| 434 | t.join().unwrap(); | |
| 435 | assert!(!signal.is_set()); | |
| 436 | ||
| 437 | // As long as we stop using fibers before thread teardown, everything works as expected. | |
| 438 | let signal2 = signal.clone(); | |
| 439 | let t = thread::spawn(move || unsafe { | |
| 440 | let mut signal = Some(signal2); | |
| 441 | let _ = ConvertThreadToFiber(ptr::null()); | |
| 442 | FOO.with(|f| { | |
| 443 | *f.get() = Some(NotifyOnDrop(signal.take().unwrap())); | |
| 444 | }); | |
| 445 | let _ = ConvertFiberToThread(); | |
| 446 | }); | |
| 447 | signal.wait(); | |
| 448 | assert!(signal.is_set()); | |
| 449 | t.join().unwrap(); | |
| 450 | } |
src/tools/miri/src/shims/tls.rs+123-14| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | //! Implement thread-local storage. |
| 2 | 2 | |
| 3 | use std::collections::BTreeMap; | |
| 4 | 3 | use std::collections::btree_map::Entry as BTreeEntry; |
| 4 | use std::collections::{BTreeMap, VecDeque}; | |
| 5 | 5 | use std::task::Poll; |
| 6 | 6 | |
| 7 | 7 | use rustc_abi::{ExternAbi, HasDataLayout, Size}; |
| ... | ... | @@ -17,18 +17,27 @@ pub type TlsKey = u128; |
| 17 | 17 | pub struct TlsEntry<'tcx> { |
| 18 | 18 | /// The data for this key. None is used to represent NULL. |
| 19 | 19 | /// (We normalize this early to avoid having to do a NULL-ptr-test each time we access the data.) |
| 20 | data: BTreeMap<ThreadId, Scalar>, | |
| 21 | dtor: Option<(ty::Instance<'tcx>, Span)>, | |
| 20 | pub data: BTreeMap<ThreadId, Scalar>, | |
| 21 | pub dtor: Option<(ty::Instance<'tcx>, Span)>, | |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | #[derive(Default, Debug)] |
| 25 | struct RunningDtorState { | |
| 25 | struct RunningPthreadDtorState { | |
| 26 | 26 | /// The last TlsKey used to retrieve a TLS destructor. `None` means that we |
| 27 | 27 | /// have not tried to retrieve a TLS destructor yet or that we already tried |
| 28 | 28 | /// all keys. |
| 29 | 29 | last_key: Option<TlsKey>, |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | #[derive(Default, Debug)] | |
| 33 | struct RunningWindowsDtorState { | |
| 34 | /// The last TlsKey for which a TLS destructor ran. `None` means that we | |
| 35 | /// have not run a TLS destructor yet. This is used to clear the TLS value after the dtor returned. | |
| 36 | last_key: Option<TlsKey>, | |
| 37 | // Keys that have destructors that we still need to run. | |
| 38 | remaining_keys: VecDeque<TlsKey>, | |
| 39 | } | |
| 40 | ||
| 32 | 41 | #[derive(Debug)] |
| 33 | 42 | pub struct TlsData<'tcx> { |
| 34 | 43 | /// The Key to use for the next thread-local allocation. |
| ... | ... | @@ -72,11 +81,11 @@ impl<'tcx> TlsData<'tcx> { |
| 72 | 81 | interp_ok(new_key) |
| 73 | 82 | } |
| 74 | 83 | |
| 75 | pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx> { | |
| 84 | pub fn delete_tls_key(&mut self, key: TlsKey) -> InterpResult<'tcx, TlsEntry<'tcx>> { | |
| 76 | 85 | match self.keys.remove(&key) { |
| 77 | Some(_) => { | |
| 86 | Some(entry) => { | |
| 78 | 87 | trace!("TLS key {} removed", key); |
| 79 | interp_ok(()) | |
| 88 | interp_ok(entry) | |
| 80 | 89 | } |
| 81 | 90 | None => throw_ub_format!("removing a nonexistent TLS key: {}", key), |
| 82 | 91 | } |
| ... | ... | @@ -220,10 +229,12 @@ enum TlsDtorsStatePriv<'tcx> { |
| 220 | 229 | #[default] |
| 221 | 230 | Init, |
| 222 | 231 | MacOsDtors, |
| 223 | PthreadDtors(RunningDtorState), | |
| 224 | /// For Windows Dtors, we store the list of functions that we still have to call. | |
| 225 | /// These are functions from the magic `.CRT$XLB` linker section. | |
| 226 | WindowsDtors(Vec<(ImmTy<'tcx>, Span)>), | |
| 232 | PthreadDtors(RunningPthreadDtorState), | |
| 233 | /// For Windows, we support two different ways dtors can be registered. | |
| 234 | /// 1. Functions that are registered via the `FlsAlloc` function, which are invoked one by one. | |
| 235 | /// 2. Functions from the magic `.CRT$XLB` linker section. | |
| 236 | /// We store these as a list of functions that we still have to call. | |
| 237 | WindowsDtors(RunningWindowsDtorState, Vec<(ImmTy<'tcx>, Span)>), | |
| 227 | 238 | Done, |
| 228 | 239 | } |
| 229 | 240 | |
| ... | ... | @@ -250,8 +261,13 @@ impl<'tcx> TlsDtorsState<'tcx> { |
| 250 | 261 | Os::Windows => { |
| 251 | 262 | // Determine which destructors to run. |
| 252 | 263 | let dtors = this.lookup_windows_tls_dtors()?; |
| 264 | ||
| 265 | // Fetch fls keys that have destructors. Keys registered during thread exit will not have their destructor called. | |
| 266 | // See also: `schedule_next_windows_fls_dtor`. | |
| 267 | let fls_keys_with_dtors = this.lookup_windows_fls_keys_with_dtors()?; | |
| 268 | ||
| 253 | 269 | // And move to the next state, that runs them. |
| 254 | break 'new_state WindowsDtors(dtors); | |
| 270 | break 'new_state WindowsDtors(RunningWindowsDtorState { last_key: None, remaining_keys: fls_keys_with_dtors }, dtors); | |
| 255 | 271 | } |
| 256 | 272 | _ => { |
| 257 | 273 | // No TLS dtor support. |
| ... | ... | @@ -273,7 +289,13 @@ impl<'tcx> TlsDtorsState<'tcx> { |
| 273 | 289 | Poll::Ready(()) => break 'new_state Done, |
| 274 | 290 | } |
| 275 | 291 | } |
| 276 | WindowsDtors(dtors) => { | |
| 292 | WindowsDtors(state, dtors) => { | |
| 293 | // Fls destructors are scheduled before the tls callback. | |
| 294 | match this.schedule_next_windows_fls_dtor(state)? { | |
| 295 | Poll::Pending => return interp_ok(Poll::Pending), // just keep going | |
| 296 | Poll::Ready(()) => {} | |
| 297 | } | |
| 298 | ||
| 277 | 299 | if let Some((dtor, span)) = dtors.pop() { |
| 278 | 300 | this.schedule_windows_tls_dtor(dtor, span)?; |
| 279 | 301 | return interp_ok(Poll::Pending); // we stay in this state (but `dtors` got shorter) |
| ... | ... | @@ -306,6 +328,22 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |
| 306 | 328 | interp_ok(this.lookup_link_section(|section| section == ".CRT$XLB")?) |
| 307 | 329 | } |
| 308 | 330 | |
| 331 | /// Lookup all the FLS keys (which are stored as TLS keys) that have a destructor. | |
| 332 | /// See also: `schedule_next_windows_fls_dtor`. | |
| 333 | fn lookup_windows_fls_keys_with_dtors(&mut self) -> InterpResult<'tcx, VecDeque<TlsKey>> { | |
| 334 | let this = self.eval_context_mut(); | |
| 335 | ||
| 336 | interp_ok( | |
| 337 | this.machine | |
| 338 | .tls | |
| 339 | .keys | |
| 340 | .iter() | |
| 341 | .filter(|(_, data)| data.dtor.is_some()) | |
| 342 | .map(|(key, _)| *key) | |
| 343 | .collect(), | |
| 344 | ) | |
| 345 | } | |
| 346 | ||
| 309 | 347 | fn schedule_windows_tls_dtor(&mut self, dtor: ImmTy<'tcx>, span: Span) -> InterpResult<'tcx> { |
| 310 | 348 | let this = self.eval_context_mut(); |
| 311 | 349 | |
| ... | ... | @@ -360,7 +398,7 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |
| 360 | 398 | /// a destructor to schedule, and `false` otherwise. |
| 361 | 399 | fn schedule_next_pthread_tls_dtor( |
| 362 | 400 | &mut self, |
| 363 | state: &mut RunningDtorState, | |
| 401 | state: &mut RunningPthreadDtorState, | |
| 364 | 402 | ) -> InterpResult<'tcx, Poll<()>> { |
| 365 | 403 | let this = self.eval_context_mut(); |
| 366 | 404 | let active_thread = this.active_thread(); |
| ... | ... | @@ -392,4 +430,75 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |
| 392 | 430 | |
| 393 | 431 | interp_ok(Poll::Ready(())) |
| 394 | 432 | } |
| 433 | ||
| 434 | /// Schedule a Windows FLS destructor, if one is found. | |
| 435 | fn schedule_next_windows_fls_dtor( | |
| 436 | &mut self, | |
| 437 | state: &mut RunningWindowsDtorState, | |
| 438 | ) -> InterpResult<'tcx, Poll<()>> { | |
| 439 | let this = self.eval_context_mut(); | |
| 440 | let active_thread = this.active_thread(); | |
| 441 | ||
| 442 | // According to [PflsCallbackFunction's docs], | |
| 443 | // > If the FLS slot is in use, `FlsCallback`` is called on .. thread exit .. | |
| 444 | // However, the exact order and semantics are not defined. | |
| 445 | // We use the following implementation, which matches both observed behavior and | |
| 446 | // Wine's implementation of the same logic in the [`RtlProcessFlsData`] function. | |
| 447 | // 1. Fetch all the keys once (in `lookup_windows_fls_keys_with_dtors`). | |
| 448 | // 2. Go over them one by one, in order, skipping keys without dtors. | |
| 449 | // 3. Fetch the value associated with the key. | |
| 450 | // 4. If it is non-zero, call the registered dtor. | |
| 451 | // 5. After the dtor is called, clear the key's value, setting it to zero. | |
| 452 | // New keys registered during thread exit are ignored, but values set before the dtor is scheduled are visible. | |
| 453 | // Keys without dtors will not be set to zero. | |
| 454 | // [PflsCallbackFunction's docs]: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-pfls_callback_function | |
| 455 | // [`RtlProcessFlsData`]: https://github.com/wine-mirror/wine/blob/wine-11.0/dlls/ntdll/thread.c#L679 | |
| 456 | ||
| 457 | // We are done running the previous key's destructor, so set it's value to zero. | |
| 458 | if let Some(last_key) = state.last_key.take() { | |
| 459 | if let Some(TlsEntry { data, .. }) = this.machine.tls.keys.get_mut(&last_key) { | |
| 460 | data.remove(&active_thread); | |
| 461 | }; | |
| 462 | } | |
| 463 | ||
| 464 | while let Some(key) = state.remaining_keys.pop_front() { | |
| 465 | // Fetch dtor for this `key`. | |
| 466 | // If the key doesn't have a dtor or does not exist any more, move on to the next key. | |
| 467 | let (data, dtor) = match this.machine.tls.keys.get(&key) { | |
| 468 | Some(TlsEntry { data, dtor: Some(dtor) }) => (data, dtor), | |
| 469 | _ => continue, | |
| 470 | }; | |
| 471 | ||
| 472 | let (instance, span) = dtor.to_owned(); | |
| 473 | ||
| 474 | // If the key has no value in this thread, move on to the next key. | |
| 475 | let ptr = match data.get(&active_thread) { | |
| 476 | Some(data_scalar) => *data_scalar, | |
| 477 | None => continue, | |
| 478 | }; | |
| 479 | ||
| 480 | assert!( | |
| 481 | ptr.to_target_usize(this).unwrap() != 0, | |
| 482 | "TLS key's value can't be null (should be absent instead)" | |
| 483 | ); | |
| 484 | ||
| 485 | trace!("Running TLS dtor {:?} on {:?} at {:?}", instance, ptr, active_thread); | |
| 486 | ||
| 487 | // We'll clear this key's value next time we are called. | |
| 488 | state.last_key = Some(key); | |
| 489 | ||
| 490 | this.call_thread_root_function( | |
| 491 | instance, | |
| 492 | ExternAbi::System { unwind: false }, | |
| 493 | &[ImmTy::from_scalar(ptr, this.machine.layouts.mut_raw_ptr)], | |
| 494 | None, | |
| 495 | span, | |
| 496 | )?; | |
| 497 | ||
| 498 | return interp_ok(Poll::Pending); | |
| 499 | } | |
| 500 | ||
| 501 | // We are done scheduling all the keys. | |
| 502 | interp_ok(Poll::Ready(())) | |
| 503 | } | |
| 395 | 504 | } |
src/tools/miri/src/shims/windows/foreign_items.rs+95| ... | ... | @@ -691,6 +691,89 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |
| 691 | 691 | this.write_int(1, dest)?; |
| 692 | 692 | } |
| 693 | 693 | |
| 694 | // Fiber-local storage - similar to TLS but supports destructors. | |
| 695 | "FlsAlloc" => { | |
| 696 | // Create key and return it. | |
| 697 | let [dtor] = this.check_shim_sig( | |
| 698 | shim_sig!(extern "system" fn(winapi::PFLS_CALLBACK_FUNCTION) -> u32), | |
| 699 | link_name, | |
| 700 | abi, | |
| 701 | args, | |
| 702 | )?; | |
| 703 | let dtor = this.read_pointer(dtor)?; | |
| 704 | ||
| 705 | // Extract the function type out of the signature (that seems easier than constructing it ourselves). | |
| 706 | let dtor = if !this.ptr_is_null(dtor)? { | |
| 707 | Some(( | |
| 708 | this.get_ptr_fn(dtor)?.as_instance()?, | |
| 709 | this.machine.current_user_relevant_span(), | |
| 710 | )) | |
| 711 | } else { | |
| 712 | None | |
| 713 | }; | |
| 714 | ||
| 715 | let key = this.machine.tls.create_tls_key(dtor, dest.layout.size)?; | |
| 716 | this.write_scalar(Scalar::from_uint(key, dest.layout.size), dest)?; | |
| 717 | } | |
| 718 | "FlsGetValue" => { | |
| 719 | let [key] = this.check_shim_sig( | |
| 720 | shim_sig!(extern "system" fn(u32) -> *mut _), | |
| 721 | link_name, | |
| 722 | abi, | |
| 723 | args, | |
| 724 | )?; | |
| 725 | let key = u128::from(this.read_scalar(key)?.to_u32()?); | |
| 726 | let active_thread = this.active_thread(); | |
| 727 | let ptr = this.machine.tls.load_tls(key, active_thread, this)?; | |
| 728 | this.write_scalar(ptr, dest)?; | |
| 729 | } | |
| 730 | "FlsSetValue" => { | |
| 731 | let [key, new_ptr] = this.check_shim_sig( | |
| 732 | shim_sig!(extern "system" fn(u32, *mut _) -> winapi::BOOL), | |
| 733 | link_name, | |
| 734 | abi, | |
| 735 | args, | |
| 736 | )?; | |
| 737 | let key = u128::from(this.read_scalar(key)?.to_u32()?); | |
| 738 | let active_thread = this.active_thread(); | |
| 739 | let new_data = this.read_scalar(new_ptr)?; | |
| 740 | this.machine.tls.store_tls(key, active_thread, new_data, &*this.tcx)?; | |
| 741 | ||
| 742 | // Return success (`1`). | |
| 743 | this.write_int(1, dest)?; | |
| 744 | } | |
| 745 | "FlsFree" => { | |
| 746 | let [key] = this.check_shim_sig( | |
| 747 | shim_sig!(extern "system" fn(u32) -> winapi::BOOL), | |
| 748 | link_name, | |
| 749 | abi, | |
| 750 | args, | |
| 751 | )?; | |
| 752 | let key = u128::from(this.read_scalar(key)?.to_u32()?); | |
| 753 | let tls_entry = this.machine.tls.delete_tls_key(key)?; | |
| 754 | ||
| 755 | // FIXME: We should run the destructor here *for all threads*. But that's non-trivial and std doesn't need it so we bail out with an "unsupported" error. | |
| 756 | if !tls_entry.data.is_empty() && tls_entry.dtor.is_some() { | |
| 757 | throw_unsup_format!( | |
| 758 | "calling `FlsFree` on a key with an associated dtor is not supported" | |
| 759 | ); | |
| 760 | } | |
| 761 | ||
| 762 | // Return success (`1`). | |
| 763 | this.write_int(1, dest)?; | |
| 764 | } | |
| 765 | "IsThreadAFiber" => { | |
| 766 | let [] = this.check_shim_sig( | |
| 767 | shim_sig!(extern "system" fn() -> winapi::BOOL), | |
| 768 | link_name, | |
| 769 | abi, | |
| 770 | args, | |
| 771 | )?; | |
| 772 | ||
| 773 | // Return FALSE, as Miri does not support fibers. | |
| 774 | this.write_int(0, dest)?; | |
| 775 | } | |
| 776 | ||
| 694 | 777 | // Access to command-line arguments |
| 695 | 778 | "GetCommandLineW" => { |
| 696 | 779 | // FIXME: This does not have a direct test (#3179). |
| ... | ... | @@ -1317,6 +1400,18 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { |
| 1317 | 1400 | // FIXME: this should return a nonzero value if this call does result in switching to another thread. |
| 1318 | 1401 | this.write_null(dest)?; |
| 1319 | 1402 | } |
| 1403 | "atexit" if this.frame_in_std() => { | |
| 1404 | let [_value] = this.check_shim_sig( | |
| 1405 | shim_sig!(extern "C" fn(*const _) -> winapi::c_int), | |
| 1406 | link_name, | |
| 1407 | abi, | |
| 1408 | args, | |
| 1409 | )?; | |
| 1410 | ||
| 1411 | // We do not support registering atexit handlers, which is used by the thread-local destructor implementation in std. | |
| 1412 | // But we also do not support manually unloading DLLs, so this has no visible effect. | |
| 1413 | this.write_int(0, dest)?; | |
| 1414 | } | |
| 1320 | 1415 | |
| 1321 | 1416 | _ => return interp_ok(EmulateItemResult::NotSupported), |
| 1322 | 1417 | } |
src/tools/miri/tests/pass/tls/windows-tls.rs+192-5| ... | ... | @@ -1,18 +1,205 @@ |
| 1 | 1 | //@only-target: windows # this directly tests windows-only functions |
| 2 | 2 | |
| 3 | 3 | use std::ffi::c_void; |
| 4 | use std::ptr; | |
| 4 | use std::{ptr, thread}; | |
| 5 | ||
| 6 | pub type BOOL = i32; | |
| 7 | pub const FALSE: BOOL = 0i32; | |
| 8 | pub const TRUE: BOOL = 1i32; | |
| 5 | 9 | |
| 6 | 10 | extern "system" { |
| 7 | 11 | fn TlsAlloc() -> u32; |
| 8 | fn TlsSetValue(key: u32, val: *mut c_void) -> i32; | |
| 12 | fn TlsSetValue(key: u32, val: *mut c_void) -> BOOL; | |
| 9 | 13 | fn TlsGetValue(key: u32) -> *mut c_void; |
| 10 | fn TlsFree(key: u32) -> i32; | |
| 14 | fn TlsFree(key: u32) -> BOOL; | |
| 15 | ||
| 16 | fn FlsAlloc(lpcallback: Option<unsafe extern "system" fn(lpflsdata: *mut c_void)>) -> u32; | |
| 17 | fn FlsSetValue(key: u32, val: *mut c_void) -> BOOL; | |
| 18 | fn FlsGetValue(key: u32) -> *mut c_void; | |
| 19 | fn FlsFree(key: u32) -> BOOL; | |
| 20 | ||
| 21 | fn IsThreadAFiber() -> BOOL; | |
| 22 | } | |
| 23 | ||
| 24 | extern "system" fn dtor_unreachable(_val: *mut c_void) { | |
| 25 | unreachable!() | |
| 26 | } | |
| 27 | ||
| 28 | fn fls_0_zero_value_does_not_run() { | |
| 29 | let key = unsafe { FlsAlloc(Some(dtor_unreachable)) }; | |
| 30 | ||
| 31 | assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 32 | assert_eq!(unsafe { FlsGetValue(key).addr() }, 1); | |
| 33 | assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(0)) }, TRUE); | |
| 34 | } | |
| 35 | ||
| 36 | fn fls_1_dtor_simple() { | |
| 37 | extern "system" fn dtor(val: *mut c_void) { | |
| 38 | assert!(!val.is_null()); | |
| 39 | println!("fls_1_dtor_simple"); | |
| 40 | ||
| 41 | // Keys are freed in-order. Without a dtor, the early key's value is not zeroed out. | |
| 42 | let early_key = val as u32; | |
| 43 | assert_eq!(unsafe { FlsGetValue(early_key).addr() }, 1); | |
| 44 | } | |
| 45 | ||
| 46 | let early_key = unsafe { FlsAlloc(None) }; | |
| 47 | let later_key = unsafe { FlsAlloc(Some(dtor)) }; | |
| 48 | ||
| 49 | assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 50 | assert_eq!(unsafe { FlsGetValue(later_key).addr() }, 1); | |
| 51 | ||
| 52 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 53 | // Will be used in the dtor to check early_key's value. | |
| 54 | assert_eq!( | |
| 55 | unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) }, | |
| 56 | TRUE | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | fn fls_2_dtor_update_value_ignored() { | |
| 61 | extern "system" fn early_dtor(val: *mut c_void) { | |
| 62 | assert!(!val.is_null()); | |
| 63 | println!("fls_2.1_early_dtor"); | |
| 64 | } | |
| 65 | ||
| 66 | extern "system" fn later_dtor(val: *mut c_void) { | |
| 67 | println!("fls_2.2_later_dtor"); | |
| 68 | ||
| 69 | // Updating a different fls slot's value doesn't cause their dtor to run, if it already did. | |
| 70 | let early_key = val as u32; | |
| 71 | ||
| 72 | // After the early key's dtor run, the key's value is zeroed out. | |
| 73 | assert_eq!(unsafe { FlsGetValue(early_key).addr() }, 0); | |
| 74 | ||
| 75 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 76 | ||
| 77 | // Registering new fls slots doesn't cause their dtor to run. | |
| 78 | let a_new_key_in_dtor = unsafe { FlsAlloc(Some(dtor_unreachable)) }; | |
| 79 | assert_eq!(unsafe { FlsSetValue(a_new_key_in_dtor, ptr::without_provenance_mut(1)) }, TRUE); | |
| 80 | } | |
| 81 | ||
| 82 | let early_key = unsafe { FlsAlloc(Some(early_dtor)) }; | |
| 83 | let later_key = unsafe { FlsAlloc(Some(later_dtor)) }; | |
| 84 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 85 | assert_eq!( | |
| 86 | unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) }, | |
| 87 | TRUE | |
| 88 | ); | |
| 89 | } | |
| 90 | ||
| 91 | fn fls_3_dtor_update_value_skipped_ignored() { | |
| 92 | extern "system" fn later_dtor(val: *mut c_void) { | |
| 93 | println!("fls_3_later_dtor"); | |
| 94 | ||
| 95 | // Updating a different fls slot's value doesn't cause their dtor to run, if it was already skipped. | |
| 96 | let early_key = val as u32; | |
| 97 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 98 | } | |
| 99 | ||
| 100 | let early_key = unsafe { FlsAlloc(Some(dtor_unreachable)) }; | |
| 101 | let later_key = unsafe { FlsAlloc(Some(later_dtor)) }; | |
| 102 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(0)) }, TRUE); | |
| 103 | assert_eq!( | |
| 104 | unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) }, | |
| 105 | TRUE | |
| 106 | ); | |
| 107 | } | |
| 108 | ||
| 109 | fn fls_4_dtor_update_value_used() { | |
| 110 | extern "system" fn early_dtor(val: *mut c_void) { | |
| 111 | println!("fls_4.1_early_dtor"); | |
| 112 | ||
| 113 | // Updating a different fls slot's value affect their dtor, if it hasn't yet run. | |
| 114 | let later_key = val as u32; | |
| 115 | assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 116 | } | |
| 117 | ||
| 118 | extern "system" fn later_dtor(_val: *mut c_void) { | |
| 119 | println!("fls_4.2_later_dtor"); | |
| 120 | } | |
| 121 | ||
| 122 | let early_key = unsafe { FlsAlloc(Some(early_dtor)) }; | |
| 123 | let later_key = unsafe { FlsAlloc(Some(later_dtor)) }; | |
| 124 | assert_eq!( | |
| 125 | unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(later_key as usize)) }, | |
| 126 | TRUE | |
| 127 | ); | |
| 128 | ||
| 129 | // Setting to zero explicitly, dtor won't run unless `early_dtor` changes this value. | |
| 130 | assert_eq!(unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(0)) }, TRUE); | |
| 131 | } | |
| 132 | ||
| 133 | fn fls_5_dtor_value() { | |
| 134 | extern "system" fn dtor(val: *mut c_void) { | |
| 135 | assert!(!val.is_null()); | |
| 136 | println!("fls_5_dtor_value"); | |
| 137 | ||
| 138 | // When the key's dtor run, the key's value equals the destructor argument. | |
| 139 | let key = val as u32; | |
| 140 | assert_eq!(unsafe { FlsGetValue(key) }, val); | |
| 141 | } | |
| 142 | ||
| 143 | let key = unsafe { FlsAlloc(Some(dtor)) }; | |
| 144 | ||
| 145 | assert_eq!(unsafe { FlsSetValue(key, ptr::without_provenance_mut(key as usize)) }, TRUE); | |
| 146 | } | |
| 147 | ||
| 148 | fn fls_6_no_dtor_does_not_zero_value() { | |
| 149 | extern "system" fn later_dtor(val: *mut c_void) { | |
| 150 | println!("fls_6_later_dtor"); | |
| 151 | ||
| 152 | // If an earlier fls slot doesn't have a dtor, the key's value is _not_ zeroed out. | |
| 153 | let early_key = val as u32; | |
| 154 | assert_ne!(unsafe { FlsGetValue(early_key).addr() }, 0); | |
| 155 | } | |
| 156 | ||
| 157 | let early_key = unsafe { FlsAlloc(None) }; | |
| 158 | let later_key = unsafe { FlsAlloc(Some(later_dtor)) }; | |
| 159 | assert_eq!(unsafe { FlsSetValue(early_key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 160 | assert_eq!( | |
| 161 | unsafe { FlsSetValue(later_key, ptr::without_provenance_mut(early_key as usize)) }, | |
| 162 | TRUE | |
| 163 | ); | |
| 164 | } | |
| 165 | ||
| 166 | fn fls_7_free() { | |
| 167 | // This dtor is not expected to be called. | |
| 168 | extern "system" fn dtor_for_freed_key(_val: *mut c_void) { | |
| 169 | println!("dtor_for_freed_key"); | |
| 170 | } | |
| 171 | let key_with_dtor = unsafe { FlsAlloc(Some(dtor_for_freed_key)) }; | |
| 172 | assert_eq!(unsafe { FlsSetValue(key_with_dtor, ptr::without_provenance_mut(0)) }, TRUE); | |
| 173 | assert_eq!(unsafe { FlsFree(key_with_dtor) }, TRUE); | |
| 174 | ||
| 175 | let key_without_dtor = unsafe { FlsAlloc(None) }; | |
| 176 | assert_eq!(unsafe { FlsSetValue(key_without_dtor, ptr::without_provenance_mut(1)) }, TRUE); | |
| 177 | assert_eq!(unsafe { FlsFree(key_without_dtor) }, TRUE); | |
| 178 | } | |
| 179 | ||
| 180 | fn fls() { | |
| 181 | assert_eq!(unsafe { IsThreadAFiber() }, FALSE); | |
| 182 | ||
| 183 | fls_0_zero_value_does_not_run(); | |
| 184 | fls_1_dtor_simple(); | |
| 185 | fls_2_dtor_update_value_ignored(); | |
| 186 | fls_3_dtor_update_value_skipped_ignored(); | |
| 187 | fls_4_dtor_update_value_used(); | |
| 188 | fls_5_dtor_value(); | |
| 189 | fls_6_no_dtor_does_not_zero_value(); | |
| 190 | fls_7_free(); | |
| 11 | 191 | } |
| 12 | 192 | |
| 13 | 193 | fn main() { |
| 14 | 194 | let key = unsafe { TlsAlloc() }; |
| 15 | assert!(unsafe { TlsSetValue(key, ptr::without_provenance_mut(1)) != 0 }); | |
| 195 | assert_eq!(unsafe { TlsSetValue(key, ptr::without_provenance_mut(1)) }, TRUE); | |
| 16 | 196 | assert_eq!(unsafe { TlsGetValue(key).addr() }, 1); |
| 17 | assert!(unsafe { TlsFree(key) != 0 }); | |
| 197 | assert_eq!(unsafe { TlsFree(key) }, TRUE); | |
| 198 | ||
| 199 | thread::spawn(|| { | |
| 200 | fls(); | |
| 201 | println!("exiting thread"); | |
| 202 | }) | |
| 203 | .join() | |
| 204 | .unwrap(); | |
| 18 | 205 | } |
src/tools/miri/tests/pass/tls/windows-tls.stdout created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | exiting thread | |
| 2 | fls_1_dtor_simple | |
| 3 | fls_2.1_early_dtor | |
| 4 | fls_2.2_later_dtor | |
| 5 | fls_3_later_dtor | |
| 6 | fls_4.1_early_dtor | |
| 7 | fls_4.2_later_dtor | |
| 8 | fls_5_dtor_value | |
| 9 | fls_6_later_dtor |
tests/run-make/dynamic-loading-cdylib/foo.rs created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | #![crate_type = "cdylib"] | |
| 2 | ||
| 3 | #[no_mangle] | |
| 4 | pub extern "C" fn extern_fn_1(a: u32, b: u32) -> u32 { | |
| 5 | println!("extern_fn_1"); | |
| 6 | a + b | |
| 7 | } | |
| 8 | ||
| 9 | struct NotifyOnDrop; | |
| 10 | ||
| 11 | impl Drop for NotifyOnDrop { | |
| 12 | fn drop(&mut self) { | |
| 13 | println!("drop"); | |
| 14 | } | |
| 15 | } | |
| 16 | ||
| 17 | #[no_mangle] | |
| 18 | pub extern "C" fn extern_fn_2(a: u32, b: u32) -> u32 { | |
| 19 | thread_local!(static FOO: NotifyOnDrop = NotifyOnDrop); | |
| 20 | FOO.with(|_foo| {}); | |
| 21 | println!("extern_fn_2"); | |
| 22 | a * b | |
| 23 | } |
tests/run-make/dynamic-loading-cdylib/load_and_unload.rs created+106| ... | ... | @@ -0,0 +1,106 @@ |
| 1 | /// Verify that we can load a cdylib, call functions from it, and then unload it. | |
| 2 | ||
| 3 | #[cfg(windows)] | |
| 4 | mod libloading { | |
| 5 | type BOOL = i32; | |
| 6 | type DWORD = u32; | |
| 7 | type HANDLE = isize; | |
| 8 | pub type HMODULE = isize; | |
| 9 | type FARPROC = *mut core::ffi::c_void; | |
| 10 | ||
| 11 | #[link(name = "kernel32")] | |
| 12 | unsafe extern "system" { | |
| 13 | fn LoadLibraryExW(filename: *const u16, file: HANDLE, flags: DWORD) -> HMODULE; | |
| 14 | fn FreeLibrary(module: HMODULE) -> BOOL; | |
| 15 | fn GetProcAddress(module: HMODULE, procname: *const u8) -> FARPROC; | |
| 16 | } | |
| 17 | ||
| 18 | fn wide_null(s: &str) -> Vec<u16> { | |
| 19 | s.encode_utf16().chain(core::iter::once(0)).collect() | |
| 20 | } | |
| 21 | ||
| 22 | fn ansi_null(s: &str) -> Vec<u8> { | |
| 23 | assert!(!s.as_bytes().contains(&0), "symbol name must not contain interior NUL"); | |
| 24 | s.as_bytes().iter().copied().chain(core::iter::once(0)).collect() | |
| 25 | } | |
| 26 | ||
| 27 | pub fn load(lib_name: &str) -> Option<HMODULE> { | |
| 28 | let filename = wide_null(&format!("{}.dll", lib_name)); | |
| 29 | let handle = unsafe { LoadLibraryExW(filename.as_ptr(), 0, 0) }; | |
| 30 | if handle == 0 { None } else { Some(handle) } | |
| 31 | } | |
| 32 | ||
| 33 | pub fn get_symbol(handle: HMODULE, name: &str) -> Option<FARPROC> { | |
| 34 | let symbol_name = ansi_null(name); | |
| 35 | let symbol = unsafe { GetProcAddress(handle, symbol_name.as_ptr()) }; | |
| 36 | if symbol.is_null() { None } else { Some(symbol) } | |
| 37 | } | |
| 38 | ||
| 39 | pub fn unload(handle: HMODULE) { | |
| 40 | unsafe { | |
| 41 | FreeLibrary(handle); | |
| 42 | } | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | #[cfg(unix)] | |
| 47 | mod libloading { | |
| 48 | use std::ffi::{c_char, c_int, c_void}; | |
| 49 | ||
| 50 | const RTLD_NOW: c_int = 2; | |
| 51 | ||
| 52 | fn cstr_null(s: &str) -> Vec<c_char> { | |
| 53 | assert!(!s.as_bytes().contains(&0), "string must not contain interior NUL"); | |
| 54 | s.as_bytes().iter().copied().chain(core::iter::once(0)).map(|b| b as c_char).collect() | |
| 55 | } | |
| 56 | ||
| 57 | pub fn load(lib_name: &str) -> Option<*mut c_void> { | |
| 58 | #[cfg(target_os = "macos")] | |
| 59 | let filename = cstr_null(&format!("lib{}.dylib", lib_name)); | |
| 60 | ||
| 61 | #[cfg(not(target_os = "macos"))] | |
| 62 | let filename = cstr_null(&format!("./lib{}.so", lib_name)); | |
| 63 | ||
| 64 | let handle = unsafe { dlopen(filename.as_ptr(), RTLD_NOW) }; | |
| 65 | if handle.is_null() { None } else { Some(handle) } | |
| 66 | } | |
| 67 | ||
| 68 | pub fn get_symbol(handle: *mut c_void, name: &str) -> Option<*mut c_void> { | |
| 69 | let symbol_name = cstr_null(name); | |
| 70 | let symbol = unsafe { dlsym(handle, symbol_name.as_ptr()) }; | |
| 71 | if symbol.is_null() { None } else { Some(symbol) } | |
| 72 | } | |
| 73 | ||
| 74 | pub fn unload(handle: *mut c_void) { | |
| 75 | unsafe { | |
| 76 | dlclose(handle); | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | #[cfg_attr(any(target_os = "linux", target_os = "android"), link(name = "dl"))] | |
| 81 | extern "C" { | |
| 82 | fn dlopen(filename: *const c_char, flags: core::ffi::c_int) -> *mut c_void; | |
| 83 | fn dlclose(handle: *mut c_void) -> core::ffi::c_int; | |
| 84 | fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | type ExternFn = unsafe extern "C" fn(u32, u32) -> u32; | |
| 89 | ||
| 90 | fn main() { | |
| 91 | let foo = libloading::load("foo").expect("Failed to load library"); | |
| 92 | println!("loaded library"); | |
| 93 | ||
| 94 | let extern_fn_1 = libloading::get_symbol(foo, "extern_fn_1").expect("Failed to find symbol"); | |
| 95 | let extern_fn_1: ExternFn = unsafe { std::mem::transmute(extern_fn_1) }; | |
| 96 | let result = unsafe { extern_fn_1(2, 3) }; | |
| 97 | println!("result of extern_fn_1(2, 3): {}", result); | |
| 98 | ||
| 99 | let extern_fn_2 = libloading::get_symbol(foo, "extern_fn_2").expect("Failed to find symbol"); | |
| 100 | let extern_fn_2: ExternFn = unsafe { std::mem::transmute(extern_fn_2) }; | |
| 101 | let result = unsafe { extern_fn_2(2, 3) }; | |
| 102 | println!("result of extern_fn_2(2, 3): {}", result); | |
| 103 | ||
| 104 | libloading::unload(foo); | |
| 105 | println!("unloaded library"); | |
| 106 | } |
tests/run-make/dynamic-loading-cdylib/output_unix.txt created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | loaded library | |
| 2 | extern_fn_1 | |
| 3 | result of extern_fn_1(2, 3): 5 | |
| 4 | extern_fn_2 | |
| 5 | result of extern_fn_2(2, 3): 6 | |
| 6 | unloaded library | |
| 7 | drop |
tests/run-make/dynamic-loading-cdylib/output_windows.txt created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | loaded library | |
| 2 | extern_fn_1 | |
| 3 | result of extern_fn_1(2, 3): 5 | |
| 4 | extern_fn_2 | |
| 5 | result of extern_fn_2(2, 3): 6 | |
| 6 | drop | |
| 7 | unloaded library |
tests/run-make/dynamic-loading-cdylib/rmake.rs created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | // Checks that dynamically loading and unloading cdylib libraries works, | |
| 2 | // both for simple functions and for functions that rely on the Rust runtime | |
| 3 | // (that use thread local storage with destructors). | |
| 4 | // | |
| 5 | // - `foo.rs` is a cdylib. | |
| 6 | // - `load_and_unload.rs` is a binary that loads and unloads the cdylib. | |
| 7 | ||
| 8 | //@ ignore-cross-compile | |
| 9 | ||
| 10 | use run_make_support::{diff, run, rustc}; | |
| 11 | ||
| 12 | fn main() { | |
| 13 | rustc().input("foo.rs").run(); | |
| 14 | ||
| 15 | rustc().crate_type("bin").crate_name("load_and_unload_bin").input("load_and_unload.rs").run(); | |
| 16 | ||
| 17 | let out_raw = run("load_and_unload_bin").stdout_utf8(); | |
| 18 | ||
| 19 | #[cfg(windows)] | |
| 20 | let output_filename = "output_windows.txt"; | |
| 21 | #[cfg(unix)] | |
| 22 | let output_filename = "output_unix.txt"; | |
| 23 | ||
| 24 | diff() | |
| 25 | .expected_file(output_filename) | |
| 26 | .actual_text("actual", out_raw) | |
| 27 | .normalize(r#"\r"#, "") | |
| 28 | .run(); | |
| 29 | } |