Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DLL delay-loading on Windows #13436

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ require "file_utils"
require "colorize"
require "crystal/digest/md5"
{% if flag?(:msvc) %}
require "./loader"
require "crystal/system/win32/visual_studio"
require "crystal/system/win32/windows_sdk"
{% end %}
Expand Down Expand Up @@ -364,6 +365,19 @@ module Crystal
link_args << lib_flags
@link_flags.try { |flags| link_args << flags }

{% if flag?(:msvc) %}
if program.has_flag?("preview_dll") && !program.has_flag?("no_win32_delay_load")
# "LINK : warning LNK4199: /DELAYLOAD:foo.dll ignored; no imports found from foo.dll"
# it is harmless to skip this error because not all import libraries are always used, much
# less the individual DLLs they refer to
link_args << "/IGNORE:4199"

Loader.search_dlls(Process.parse_arguments_windows(link_args.join(' '))).each do |dll|
link_args << "/DELAYLOAD:#{dll}"
end
end
{% end %}

args = %(/nologo #{object_arg} #{output_arg} /link #{link_args.join(' ')}).gsub("\n", " ")
cmd = "#{linker} #{args}"

Expand Down
2 changes: 0 additions & 2 deletions src/compiler/crystal/loader.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ require "option_parser"
# finding symbols inside them.
#
# See system-specific implementations in ./loader for details.
#
# A Windows implementation is not yet available.
class Crystal::Loader
class LoadError < Exception
property args : Array(String)?
Expand Down
44 changes: 35 additions & 9 deletions src/compiler/crystal/loader/msvc.cr
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,42 @@ class Crystal::Loader

# Parses linker arguments in the style of `link.exe`.
def self.parse(args : Array(String), *, search_paths : Array(String) = default_search_paths) : self
libnames = [] of String
search_paths, libnames = parse_args(args, search_paths)
file_paths = [] of String

begin
self.new(search_paths, libnames, file_paths)
rescue exc : LoadError
exc.args = args
exc.search_paths = search_paths
raise exc
end
end

# Returns the list of DLLs imported from the libraries specified in the given
# linker arguments. Used by the compiler for delay-loaded DLL support.
def self.search_dlls(args : Array(String), *, search_paths : Array(String) = default_search_paths) : Set(String)
search_paths, libnames = parse_args(args, search_paths)
dlls = Set(String).new

libnames.each do |libname|
search_paths.each do |directory|
library_path = File.join(directory, library_filename(libname))
next unless File.file?(library_path)

Crystal::System::LibraryArchive.imported_dlls(library_path).each do |dll|
dlls << dll unless dll.compare("kernel32.dll", case_insensitive: true).zero?
end
break
end
end

dlls
end

private def self.parse_args(args, search_paths)
libnames = [] of String

# NOTE: `/LIBPATH`s are prepended before the default paths:
# (https://docs.microsoft.com/en-us/cpp/build/reference/libpath-additional-libpath)
#
Expand All @@ -39,14 +72,7 @@ class Crystal::Loader
end

search_paths = extra_search_paths + search_paths

begin
self.new(search_paths, libnames, file_paths)
rescue exc : LoadError
exc.args = args
exc.search_paths = search_paths
raise exc
end
{search_paths, libnames}
end

def self.library_filename(libname : String) : String
Expand Down
1 change: 1 addition & 0 deletions src/crystal/main.cr
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ end

{% if flag?(:win32) %}
require "./system/win32/wmain"
require "./system/win32/delay_load"
{% end %}

{% if flag?(:wasi) %}
Expand Down
205 changes: 205 additions & 0 deletions src/crystal/system/win32/delay_load.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
require "c/delayimp"

private ERROR_SEVERITY_ERROR = 0xC0000000_u32
private FACILITY_VISUALCPP = 0x6d

private macro vcpp_exception(err)
{{ ERROR_SEVERITY_ERROR | (FACILITY_VISUALCPP << 16) | WinError.constant(err.id) }}
end

lib LibC
$image_base = __ImageBase : IMAGE_DOS_HEADER
end

private macro p_from_rva(rva)
Copy link
Contributor Author

@HertzDevil HertzDevil May 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is just one caveat: for some reason LibC.image_base cannot be accessed from any LLVM module scope other than the top-level one, otherwise MSVC raises an internal linker error, and the only way out is to pass --single-module. So this cannot be a class method of Crystal::System::DelayLoad, and neither can __delayLoadHelper2 itself

pointerof(LibC.image_base).as(UInt8*) + {{ rva }}
end

module Crystal::System::DelayLoad
@[Extern]
record InternalImgDelayDescr,
grAttrs : LibC::DWORD,
szName : LibC::LPSTR,
phmod : LibC::HMODULE*,
pIAT : LibC::IMAGE_THUNK_DATA*,
pINT : LibC::IMAGE_THUNK_DATA*,
pBoundIAT : LibC::IMAGE_THUNK_DATA*,
pUnloadIAT : LibC::IMAGE_THUNK_DATA*,
dwTimeStamp : LibC::DWORD

@[AlwaysInline]
def self.pinh_from_image_base(hmod : LibC::HMODULE)
(hmod.as(UInt8*) + hmod.as(LibC::IMAGE_DOS_HEADER*).value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*)
end

@[AlwaysInline]
def self.interlocked_exchange(atomic : LibC::HMODULE*, value : LibC::HMODULE)
Atomic::Ops.atomicrmw(:xchg, atomic, value, :sequentially_consistent, false)
end
end

# This is a port of the default delay-load helper function in the `DelayHlp.cpp`
# file that comes with Microsoft Visual C++, except that all user-defined hooks
# are omitted. It is called every time the program attempts to load a symbol
# from a DLL. For more details see:
# https://learn.microsoft.com/en-us/cpp/build/reference/understanding-the-helper-function
#
# It is available even when the `preview_dll` flag is absent, so that system
# DLLs such as `advapi32.dll` and shards can be delay-loaded in the usual mixed
# static/dynamic builds by passing the appropriate linker flags explicitly.
#
# The delay load helper cannot call functions from the library being loaded, as
# that leads to an infinite recursion. In particular, if `preview_dll` is in
# effect, `Crystal::System.print_error` will not work, because the C runtime
# library DLLs are also delay-loaded and `LibC.snprintf` is unavailable. If you
# want print debugging inside this function, try the following:
#
# ```
# lib LibC
# STD_OUTPUT_HANDLE = -11
#
# fun GetStdHandle(nStdHandle : DWORD) : HANDLE
# fun FormatMessageA(dwFlags : DWORD, lpSource : Void*, dwMessageId : DWORD, dwLanguageId : DWORD, lpBuffer : LPSTR, nSize : DWORD, arguments : Void*) : DWORD
# end
#
# buf = uninitialized LibC::CHAR[512]
# args = StaticArray[dli.szDll, dli.dlp.union.szProcName]
# len = LibC.FormatMessageA(LibC::FORMAT_MESSAGE_FROM_STRING | LibC::FORMAT_MESSAGE_ARGUMENT_ARRAY, "Loading `%2` from `%1`\n", 0, 0, buf, buf.size, args)
# LibC.WriteFile(LibC.GetStdHandle(LibC::STD_OUTPUT_HANDLE), buf, len, out _, nil)
# ```
#
# `kernel32.dll` is the only DLL guaranteed to be available. It cannot be
# delay-loaded and the Crystal compiler excludes it from the linker arguments.
#
# This function does _not_ work with the empty prelude yet!
fun __delayLoadHelper2(pidd : LibC::ImgDelayDescr*, ppfnIATEntry : LibC::FARPROC*) : LibC::FARPROC
# TODO: support protected delay load? (/GUARD:CF)
# DloadAcquireSectionWriteAccess

# Set up some data we use for the hook procs but also useful for our own use
idd = Crystal::System::DelayLoad::InternalImgDelayDescr.new(
grAttrs: pidd.value.grAttrs,
szName: p_from_rva(pidd.value.rvaDLLName).as(LibC::LPSTR),
phmod: p_from_rva(pidd.value.rvaHmod).as(LibC::HMODULE*),
pIAT: p_from_rva(pidd.value.rvaIAT).as(LibC::IMAGE_THUNK_DATA*),
pINT: p_from_rva(pidd.value.rvaINT).as(LibC::IMAGE_THUNK_DATA*),
pBoundIAT: p_from_rva(pidd.value.rvaBoundIAT).as(LibC::IMAGE_THUNK_DATA*),
pUnloadIAT: p_from_rva(pidd.value.rvaUnloadIAT).as(LibC::IMAGE_THUNK_DATA*),
dwTimeStamp: pidd.value.dwTimeStamp,
)

dli = LibC::DelayLoadInfo.new(
cb: sizeof(LibC::DelayLoadInfo),
pidd: pidd,
ppfn: ppfnIATEntry,
szDll: idd.szName,
dlp: LibC::DelayLoadProc.new,
hmodCur: LibC::HMODULE.null,
pfnCur: LibC::FARPROC.null,
dwLastError: LibC::DWORD.zero,
)

if 0 == idd.grAttrs & LibC::DLAttrRva
rgpdli = pointerof(dli)

# DloadReleaseSectionWriteAccess

LibC.RaiseException(
vcpp_exception(ERROR_INVALID_PARAMETER),
0,
1,
pointerof(rgpdli).as(LibC::ULONG_PTR*),
)
end

hmod = idd.phmod.value

# Calculate the index for the IAT entry in the import address table
# N.B. The INT entries are ordered the same as the IAT entries so
# the calculation can be done on the IAT side.
iIAT = ppfnIATEntry.as(LibC::IMAGE_THUNK_DATA*) - idd.pIAT
iINT = iIAT

pitd = idd.pINT + iINT

dli.dlp.fImportByName = pitd.value.u1.ordinal & LibC::IMAGE_ORDINAL_FLAG == 0

if dli.dlp.fImportByName
import_by_name = p_from_rva(LibC::RVA.new!(pitd.value.u1.addressOfData))
dli.dlp.union.szProcName = import_by_name + offsetof(LibC::IMAGE_IMPORT_BY_NAME, @name)
else
dli.dlp.union.dwOrdinal = LibC::DWORD.new!(pitd.value.u1.ordinal & 0xFFFF)
end

# Check to see if we need to try to load the library.
if !hmod
# note: ANSI variant used here
unless hmod = LibC.LoadLibraryExA(dli.szDll, nil, 0)
dli.dwLastError = LibC.GetLastError

rgpdli = pointerof(dli)

# DloadReleaseSectionWriteAccess
LibC.RaiseException(
vcpp_exception(ERROR_MOD_NOT_FOUND),
0,
1,
pointerof(rgpdli).as(LibC::ULONG_PTR*),
)

# If we get to here, we blindly assume that the handler of the exception
# has magically fixed everything up and left the function pointer in
# dli.pfnCur.
return dli.pfnCur
end

# Store the library handle. If it is already there, we infer
# that another thread got there first, and we need to do a
# FreeLibrary() to reduce the refcount
hmodT = Crystal::System::DelayLoad.interlocked_exchange(idd.phmod, hmod)
LibC.FreeLibrary(hmod) if hmodT == hmod
end

# Go for the procedure now.
dli.hmodCur = hmod
if pidd.value.rvaBoundIAT != 0 && pidd.value.dwTimeStamp != 0
# bound imports exist...check the timestamp from the target image
pinh = Crystal::System::DelayLoad.pinh_from_image_base(hmod)

if pinh.value.signature == LibC::IMAGE_NT_SIGNATURE &&
pinh.value.fileHeader.timeDateStamp == idd.dwTimeStamp &&
hmod.address == pinh.value.optionalHeader.imageBase
# Everything is good to go, if we have a decent address
# in the bound IAT!
if pfnRet = LibC::FARPROC.new(idd.pBoundIAT[iIAT].u1.function)
ppfnIATEntry.value = pfnRet
# DloadReleaseSectionWriteAccess
return pfnRet
end
end
end

unless pfnRet = LibC.GetProcAddress(hmod, dli.dlp.union.szProcName)
dli.dwLastError = LibC.GetLastError

rgpdli = pointerof(dli)

# DloadReleaseSectionWriteAccess
LibC.RaiseException(
vcpp_exception(ERROR_PROC_NOT_FOUND),
0,
1,
pointerof(rgpdli).as(LibC::ULONG_PTR*),
)
# DloadAcquireSectionWriteAccess

# If we get to here, we blindly assume that the handler of the exception
# has magically fixed everything up and left the function pointer in
# dli.pfnCur.
pfnRet = dli.pfnCur
end

ppfnIATEntry.value = pfnRet
# DloadReleaseSectionWriteAccess
pfnRet
end
40 changes: 40 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/delayimp.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require "c/libloaderapi"
require "c/winnt"

lib LibC
alias RVA = DWORD

struct ImgDelayDescr
grAttrs : DWORD # attributes
rvaDLLName : RVA # RVA to dll name
rvaHmod : RVA # RVA of module handle
rvaIAT : RVA # RVA of the IAT
rvaINT : RVA # RVA of the INT
rvaBoundIAT : RVA # RVA of the optional bound IAT
rvaUnloadIAT : RVA # RVA of optional copy of original IAT
dwTimeStamp : DWORD # 0 if not bound, O.W. date/time stamp of DLL bound to (Old BIND)
end

DLAttrRva = 0x1

union DelayLoadProc_union
szProcName : LPSTR
dwOrdinal : DWORD
end

struct DelayLoadProc
fImportByName : BOOL
union : DelayLoadProc_union
end

struct DelayLoadInfo
cb : DWORD # size of structure
pidd : ImgDelayDescr* # raw form of data (everything is there)
ppfn : FARPROC* # points to address of function to load
szDll : LPSTR # name of dll
dlp : DelayLoadProc # name or ordinal of procedure
hmodCur : HMODULE # the hInstance of the library we have loaded
pfnCur : FARPROC # the actual function that will be called
dwLastError : DWORD # error received (if an error notification)
end
end
1 change: 1 addition & 0 deletions src/lib_c/x86_64-windows-msvc/c/errhandlingapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ lib LibC

fun GetLastError : DWORD
fun SetLastError(dwErrCode : DWORD)
fun RaiseException(dwExceptionCode : DWORD, dwExceptionFlags : DWORD, nNumberOfArguments : DWORD, lpArguments : ULONG_PTR*)
fun AddVectoredExceptionHandler(first : DWORD, handler : PVECTORED_EXCEPTION_HANDLER) : Void*
end
1 change: 1 addition & 0 deletions src/lib_c/x86_64-windows-msvc/c/libloaderapi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require "c/winnt"
lib LibC
alias FARPROC = Void*

fun LoadLibraryExA(lpLibFileName : LPSTR, hFile : HANDLE, dwFlags : DWORD) : HMODULE
fun LoadLibraryExW(lpLibFileName : LPWSTR, hFile : HANDLE, dwFlags : DWORD) : HMODULE
fun FreeLibrary(hLibModule : HMODULE) : BOOL

Expand Down
Loading