Let me try to explain it another way: @convention(c) (HINSTANCE, DWORD, LPVOID) -> BOOLis actually a pointer already. withUnsafePointer<T>(to: DllMain) will indeed product a local/temporary value of type UnsafePointer<@convention(c) (HINSTANCE, DWORD, LPVOID) -> BOOL>, which is a pointer to a function pointer.
Note the lack of a second UnsafePointer. Unlike C, Swift does not conflate expressions of function type with pointers to functions. The runtime representation of a value of function type might be a simple pointer to the first instruction of the function. But Swift does not leak this runtime representation into the user-visible type system. The type of this value is () -> Void, notUnsafePointer<() -> Void>.
This does not guarantee that the pointee of this pointer has the same bit pattern as GetProcAddress(hInst, "DllMain"). And itâs not legal to escape the pointer from the closure passed to withUnsafePointer(to:) anyway, so the point is moot.
(Rereading the above, it seems like we are actually in violent agreement?)
Firstly, we're talking about a @convention(c) function here, not a Swift function. @convention(c) functions are function pointers, and we dodocument them as such:
The c argument indicates a C function reference. The function value carries no context and uses the C calling convention.
Apple's documentation more clearly states that @convention(c) is equivalent to a C function pointer:
C function pointers are imported into Swift as closures with the C function pointer calling convention, denoted by the @convention(c) attribute. For example, a function pointer that has the type int (*)(void) in C is imported into Swift as @convention(c) () -> Int32.
Secondly, we're fundamentally talking about how to pass a reference to DllMain to a WinAPI function, which is a C function with C semantics, and which takes a function pointer that must be cast to a data pointer. In the context of this thread, it is important to understand that passing the pointer yielded by withUnsafePointer(to: DllMain) will not produce the intended result because it will produce a pointer to the function reference (which, again, is actually a function pointer and whose bit pattern is the actual value you want to pass to the WinAPI function.)
Yes, I agree with this. I think what threw me off is when you described the result as a pointer to a pointer to the function, which mixes and matches Swift and C worldviews. A pointer to a pointer to a function, in Swift, would be spelled UnsafePointer<UnsafePointer<FnType>>. What this function passes the closure is a pointer to a value of function type. The fact that the value happens to take the form of a single word whose bit pattern can be interpreted as the address of the first instruction of the functionâs body is an implementation detail of the runtime. This isnât even necessarily the âtrueâ address of the functionâthe OS is free to define its own semantics there.
That's fair, although the whole idea of writing DllMain in Swift is already dragging us into the sordid world of language mixology.
I've said elsewhere (somewhere?) that Swift's story for C function interop is incomplete. We should have some mechanism to say "cast a function pointer to/from a raw data pointer" without having to resort to unsafeBitCast()[1]. Then the fact that they're function pointers in trenchcoats would no longer matter because the API can abstract the ABI away. As it stands, it is necessary for an @convention(c) function to be represented as a function pointer lest the entire world fall apart at our feet as soon as we try to do anything with the OS involving a callback.
I'm admittedly ignoring the possibility of OS-side thunks and whatnot here, but I don't think they're relevant to this thread?
It would have to be generic (variadic generic, even)âŠ
âŠbut Swift generics are not monomorphized, so you end up with a C function pointer type with generics in it at runtime
âŠunless the compiler is taught (along with the runtime?) that a C function pointer with generics in it is fine, it just has a consistent representation and you just canât get the generics back out from a plain value.
âŠwhich, unfortunately, I believe is hard (but havenât directly looked into even when I was on the compiler team).
One thing that could work is a magic function like withoutActuallyEscaping, which has compiler support to make sure the outer and inner types match up and is guaranteed to be inlined away. But I donât know if we want more of those for this relatively esoteric operation, especially one that is only guaranteed by POSIX (and Windows), not inherent to all platforms. (Is it true for wasm? I forget.)
Never said it was easy! And I'm not trying to design it here. But it remains a soft spot that I would hope we can address someday. Swift Testing needs to cast function pointers pretty frequently!
Function pointers are representable as data pointers in Wasm, yeah, although just in the sense that they fit in a word, so it's not like you can read bytes from them.
In general, conversion between data and function pointers is undefined in the C standard, but almost all 32-bit and wider systems do define it, and more importantly the (non-Embedded) Swift ABI depends on it!
Additionally, an unrelated issue I had was that another exported function was taking a struct, which I assumed meant it was being passed by value inside the stack. But apparently structs are automatically converted to reference, or perhaps a reference to a copy. The fact that @_cdecl prevents mapping the contents of the struct with a tuple, even though the actual parameter is just a pointer, is a bit annoying. But I can always use a raw pointer and then cast.
The final remaining issue I have is that for some reason the DLL would not unload when attempting to close the application, forcing me to kill the application instead. Something gets stuck during unload, and it gets stuck even if I stub out all the exports. It wasn't happening with stubbed out exports in Swift 5.10, so I can only conclude that this is something inherent to the Swift 6 runtime. However, I am at a complete loss as to how to debug such a thing.
Iâd start by attaching windbg and figuring out which threads are still alive (~ command) and then getting a stack trace for those threads (~«thread id» k command).
Hmm, so what seems the most related is FreeLibrary is calling Foundation!$s10Foundation6NSUUIDC20_toCustomAnyHashables0eF0VSgyF (There has to be a decent way to demangle this), which ends up stuck in KERNELBASE!CompareObjectHandles. It's somewhat of an issue that I have no idea what this is supposed to do, or why it runs when attempting to free the library, let alone what's causing it to become stuck.
Diving into Foundation source code would be somewhat of a leap in difficulty.
You might need to artificially increase the ref count of your DLL. But this may not be safe to do from DllMain since that is called with the loader lock held.
Did you build Foundation from source? I think that the stack is likely misleading as it would only show exported functions without the debug information (which would requre a debug buld). It isn't currently feasible to distribute the symbols in the installer since they amount to ~20G.