diff --git a/src/compiler/crystal/interpreter/context.cr b/src/compiler/crystal/interpreter/context.cr index 45dcaaf12bf8..31eb54ba0792 100644 --- a/src/compiler/crystal/interpreter/context.cr +++ b/src/compiler/crystal/interpreter/context.cr @@ -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: @@ -477,6 +484,19 @@ 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 @@ -484,9 +504,32 @@ class Crystal::Repl::Context 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 diff --git a/src/compiler/crystal/loader/mingw.cr b/src/compiler/crystal/loader/mingw.cr index 2c557a893640..4bda7c7cc3ad 100644 --- a/src/compiler/crystal/loader/mingw.cr +++ b/src/compiler/crystal/loader/mingw.cr @@ -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 @@ -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 @@ -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 @@ -101,7 +111,31 @@ 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 @@ -109,6 +143,33 @@ class Crystal::Loader 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