Skip to content
Open
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
43 changes: 43 additions & 0 deletions src/compiler/crystal/interpreter/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,17 @@ class Crystal::Repl::Context

getter? loader : Loader?

# Tracks the lib_flags string used at loader initialization. Used to detect
# new @[Link] annotations added during incremental REPL evaluation (e.g. the
# first use of Regex in the REPL adds pcre2 to lib_flags after the loader
# was already initialized with only the prelude's libraries).
@loader_lib_flags = ""

getter(loader : Loader) {
lib_flags = program.lib_flags
# Execute and expand `subcommands`.
lib_flags = lib_flags.gsub(/`(.*?)`/) { `#{$1}`.chomp }
@loader_lib_flags = lib_flags

args = Process.parse_arguments(lib_flags)
# FIXME: Part 1: This is a workaround for initial integration of the interpreter:
Expand Down Expand Up @@ -477,16 +484,52 @@ class Crystal::Repl::Context
paths << path
end

paths
{% elsif flag?(:win32) && flag?(:gnu) %}
# MinGW: include the crystal.exe directory (where DLLs live) and PATH
paths = [] of String

if executable_path = Process.executable_path
paths << File.dirname(executable_path)
end

ENV["PATH"]?.try &.split(Process::PATH_DELIMITER, remove_empty: true) do |path|
paths << path
end

paths
{% else %}
nil
{% end %}
end

def c_function(name : String)
# In REPL mode, new @[Link] annotations may be added to lib_flags after the
# loader was already initialized (e.g. Regex / pcre2 on first regex use).
# Sync the loader with any new libraries before looking up the symbol.
if loader?
current = program.lib_flags
update_loader(current) if current != @loader_lib_flags
end
loader.find_symbol(name)
end

# Loads any libraries that appear in *new_lib_flags* but were not present
# when the loader was last initialized.
private def update_loader(new_lib_flags : String)
expanded = new_lib_flags.gsub(/`(.*?)`/) { `#{$1}`.chomp }
old_args = Process.parse_arguments(@loader_lib_flags).to_set

Process.parse_arguments(expanded).each do |arg|
next if old_args.includes?(arg)
if libname = arg.lchop?("-l")
loader.load_library?(libname)
end
end

@loader_lib_flags = expanded
end

def align(size : Int32) : Int32
rem = size.remainder(8)
if rem == 0
Expand Down
69 changes: 65 additions & 4 deletions src/compiler/crystal/loader/mingw.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ require "crystal/system/win32/library_archive"
class Crystal::Loader
alias Handle = Void*

def initialize(@search_paths : Array(String))
def initialize(@search_paths : Array(String), @dll_search_paths : Array(String)? = nil)
end

# Parses linker arguments in the style of `ld`.
#
# This is identical to the Unix loader. *dll_search_paths* has no effect.
# This is identical to the Unix loader. *dll_search_paths* is used to locate
# DLLs by full path before falling back to bare-name loading.
def self.parse(args : Array(String), *, search_paths : Array(String) = default_search_paths, dll_search_paths : Array(String)? = nil) : self
libnames = [] of String
file_paths = [] of String
Expand Down Expand Up @@ -63,7 +64,7 @@ class Crystal::Loader
libnames << crt_dll

begin
loader = new(search_paths)
loader = new(search_paths, dll_search_paths)
loader.load_all(libnames, file_paths)
loader
rescue exc : LoadError
Expand All @@ -82,6 +83,15 @@ class Crystal::Loader
address = LibC.GetProcAddress(handle, name.check_no_null_byte)
return address if address
end

if ENV["CRYSTAL_INTERPRETER_LOADER_INFO"]?.presence
STDERR.puts " find_symbol?(#{name}): not found in #{@handles.size} handle(s)"
@handles.each_with_index do |h, i|
STDERR.puts " handle[#{i}]=0x#{h.address.to_s(16)} lib=#{@loaded_libraries[i]? || "?"}"
end
end

nil
end

def load_file(path : String | ::Path) : Nil
Expand All @@ -101,14 +111,65 @@ class Crystal::Loader
end

private def load_dll?(dll)
handle = open_library(dll)
# For DLLs that are already loaded in the process (e.g. libpcre2-8-0.dll
# as a load-time dep of crystal.exe), LoadLibraryExW with a bare name can
# return a module handle that doesn't work with GetProcAddress. Try
# GetModuleHandleExW first: it returns the canonical HMODULE that the
# Windows loader already tracks for the named DLL, and GetProcAddress works
# on it reliably.
via_get_module = false
handle = if LibC.GetModuleHandleExW(0, System.to_wstr(dll), out existing) != 0
via_get_module = true
existing.as(Handle)
else
# DLL not yet in process: load by full path when possible to avoid the
# same duplicate-handle issue if the DLL appears in the process later.
full_path = resolve_dll_full_path(dll)
open_library(full_path || dll)
end

if ENV["CRYSTAL_INTERPRETER_LOADER_INFO"]?.presence
if handle
STDERR.puts " load_dll?(#{dll}): handle=0x#{handle.address.to_s(16)} via_GetModuleHandleExW=#{via_get_module} path=#{module_filename(handle) || "?"}"
else
STDERR.puts " load_dll?(#{dll}): FAILED (handle is null)"
end
end

return false unless handle

@handles << handle
@loaded_libraries << (module_filename(handle) || dll)
true
end

# Searches for *dll* by full path in dll_search_paths and in sibling `bin/`
# directories relative to each library search path.
# Returns the full path if found, or nil to fall back to the bare name.
private def resolve_dll_full_path(dll : String) : String?
# Skip if it's already a path (contains a separator)
return nil if ::Path::SEPARATORS.any? { |sep| dll.includes?(sep) }

# Search in explicit dll_search_paths first
@dll_search_paths.try &.each do |dir|
path = File.join(dir, dll)
return path if File.file?(path)
end

# Search in library search paths and their sibling bin/ directories.
# In a typical MinGW layout, .dll.a files live in lib/ and .dll files in bin/,
# so for each search path like D:\Crystal\lib\ we also check D:\Crystal\bin\.
@search_paths.each do |lib_dir|
bin_dir = File.join(::Path[lib_dir].parent.to_s, "bin")
{% for dir in %w(bin_dir lib_dir) %}
path = File.join({{dir.id}}, dll)
return path if File.file?(path)
{% end %}
end

nil
end

def load_library(libname : String) : Nil
load_library?(libname) || raise LoadError.new "cannot find #{Loader.library_filename(libname)}"
end
Expand Down