Skip to content

Commit

Permalink
Support DLL delay-loading on Windows (#13436)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed May 11, 2023
1 parent a515985 commit 39aae80
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 11 deletions.
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)
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

0 comments on commit 39aae80

Please sign in to comment.