Skip to content

Commit

Permalink
Support -static and -dynamic .lib suffixes on Windows (#13473)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored May 23, 2023
1 parent 5c4dc9c commit f690598
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 24 deletions.
31 changes: 23 additions & 8 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,29 @@ module Crystal
@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}"
unless @cross_compile
extra_suffix = program.has_flag?("preview_dll") ? "-dynamic" : "-static"
search_result = Loader.search_libraries(Process.parse_arguments_windows(link_args.join(' ').gsub('\n', ' ')), extra_suffix: extra_suffix)
if not_found = search_result.not_found?
error "Cannot locate the .lib files for the following libraries: #{not_found.join(", ")}"
end

link_args = search_result.remaining_args.concat(search_result.library_paths).map { |arg| Process.quote_windows(arg) }

if !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"

dlls = Set(String).new
search_result.library_paths.each do |library_path|
Crystal::System::LibraryArchive.imported_dlls(library_path).each do |dll|
dlls << dll.downcase
end
end
dlls.delete "kernel32.dll"
dlls.each { |dll| link_args << "/DELAYLOAD:#{dll}" }
end
end
{% end %}
Expand Down
57 changes: 43 additions & 14 deletions src/compiler/crystal/loader/msvc.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,53 @@ class Crystal::Loader
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
struct SearchLibResult
getter library_paths = [] of String
getter remaining_args = [] of String
getter(not_found) { [] of String }

def not_found?
@not_found
end
end

# Extracts the command-line arguments from *args* that add libraries and
# expands them to their absolute paths. Returns a `SearchLibResult` with those
# expanded paths, plus unused arguments and libraries that were not found.
def self.search_libraries(args : Array(String), *, search_paths : Array(String) = default_search_paths, extra_suffix : String? = nil) : SearchLibResult
result = SearchLibResult.new
search_paths, libnames = parse_args(args, search_paths, remaining: result.remaining_args)

libnames.each do |libname|
search_paths.each do |directory|
library_path = File.join(directory, library_filename(libname))
next unless File.file?(library_path)
if found_path = search_library(libname, search_paths, extra_suffix)
result.library_paths << found_path
else
result.not_found << libname
end
end

result
end

Crystal::System::LibraryArchive.imported_dlls(library_path).each do |dll|
dlls << dll unless dll.compare("kernel32.dll", case_insensitive: true).zero?
private def self.search_library(libname, search_paths, extra_suffix)
if ::Path::SEPARATORS.any? { |separator| libname.includes?(separator) }
libname = File.expand_path(libname)
library_path = library_filename(libname)
return library_path if File.file?(library_path)
else
search_paths.each do |directory|
if extra_suffix
library_path = File.join(directory, library_filename(libname + extra_suffix))
return library_path if File.file?(library_path)
end
break

library_path = File.join(directory, library_filename(libname))
return library_path if File.file?(library_path)
end
end

dlls
end

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

# NOTE: `/LIBPATH`s are prepended before the default paths:
Expand All @@ -68,10 +93,14 @@ class Crystal::Loader
extra_search_paths << lib_path
elsif !arg.starts_with?('/') && (name = arg.rchop?(".lib"))
libnames << name
elsif remaining
remaining << arg
end
end

search_paths = extra_search_paths + search_paths
search_paths.uniq! &.downcase
libnames.uniq! &.downcase
{search_paths, libnames}
end

Expand Down
7 changes: 5 additions & 2 deletions src/crystal/system/win32/library_archive.cr
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ module Crystal::System::LibraryArchive
sig2 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian)
return unless sig2 == 0xFFFF

# version(2) + machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2)
io.skip(16)
version = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian)
return unless version == 0 # 1 and 2 are used by object files (ANON_OBJECT_HEADER)

# machine(2) + time(4) + size(4) + ordinal/hint(2) + flags(2)
io.skip(14)

# TODO: is there a way to do this without constructing a temporary string,
# but with the optimizations present in `IO#gets`?
Expand Down

0 comments on commit f690598

Please sign in to comment.