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 @[Link]'s DLL search order in the interpreter on Windows #14146

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
32 changes: 24 additions & 8 deletions spec/compiler/ffi/ffi_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ private record TestStruct,
d : Float64,
p : Pointer(Void)

private def dll_search_paths
{% if flag?(:msvc) %}
[SPEC_CRYSTAL_LOADER_LIB_PATH]
{% else %}
nil
{% end %}
end

{% if flag?(:unix) %}
class Crystal::Loader
def self.new(search_paths : Array(String), *, dll_search_paths : Nil)
new(search_paths)
end
end
{% end %}

describe Crystal::FFI::CallInterface do
before_all do
FileUtils.mkdir_p(SPEC_CRYSTAL_LOADER_LIB_PATH)
Expand All @@ -33,7 +49,7 @@ describe Crystal::FFI::CallInterface do
it "simple call" do
call_interface = Crystal::FFI::CallInterface.new Crystal::FFI::Type.sint64, [] of Crystal::FFI::Type

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("answer")
return_value = 0_i64
Expand All @@ -48,7 +64,7 @@ describe Crystal::FFI::CallInterface do
Crystal::FFI::Type.sint32, Crystal::FFI::Type.sint32, Crystal::FFI::Type.sint32,
] of Crystal::FFI::Type

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum")

Expand All @@ -71,7 +87,7 @@ describe Crystal::FFI::CallInterface do
Crystal::FFI::Type.pointer,
] of Crystal::FFI::Type

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum_primitive_types")

Expand Down Expand Up @@ -109,7 +125,7 @@ describe Crystal::FFI::CallInterface do
]
call_interface = Crystal::FFI::CallInterface.new Crystal::FFI::Type.struct(struct_fields), struct_fields

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("make_struct")

Expand Down Expand Up @@ -145,7 +161,7 @@ describe Crystal::FFI::CallInterface do
]),
] of Crystal::FFI::Type

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum_struct")

Expand Down Expand Up @@ -183,7 +199,7 @@ describe Crystal::FFI::CallInterface do
]),
] of Crystal::FFI::Type

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum_array")

Expand All @@ -208,7 +224,7 @@ describe Crystal::FFI::CallInterface do
it "basic" do
call_interface = Crystal::FFI::CallInterface.variadic Crystal::FFI::Type.sint64, [Crystal::FFI::Type.sint32, Crystal::FFI::Type.sint32, Crystal::FFI::Type.sint32, Crystal::FFI::Type.sint32] of Crystal::FFI::Type, 1

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum_variadic")

Expand All @@ -224,7 +240,7 @@ describe Crystal::FFI::CallInterface do
it "zero varargs" do
call_interface = Crystal::FFI::CallInterface.variadic Crystal::FFI::Type.sint64, [Crystal::FFI::Type.sint32] of Crystal::FFI::Type, 1

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH])
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: dll_search_paths)
loader.load_library "sum"
function_pointer = loader.find_symbol("sum_variadic")

Expand Down
65 changes: 53 additions & 12 deletions spec/compiler/loader/msvc_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe Crystal::Loader do

describe "#load_file?" do
it "finds function symbol" do
loader = Crystal::Loader.new([] of String)
loader = Crystal::Loader.new([] of String, dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_file?(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, Crystal::Loader.library_filename("foo"))).should be_true
loader.find_symbol?("foo").should_not be_nil
ensure
Expand All @@ -55,15 +55,15 @@ describe Crystal::Loader do

describe "#load_library?" do
it "library name" do
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_library?("foo").should be_true
loader.find_symbol?("foo").should_not be_nil
ensure
loader.close_all if loader
end

it "full path" do
loader = Crystal::Loader.new([] of String)
loader = Crystal::Loader.new([] of String, dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_library?(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, Crystal::Loader.library_filename("foo"))).should be_true
loader.find_symbol?("foo").should_not be_nil
ensure
Expand All @@ -72,7 +72,7 @@ describe Crystal::Loader do

it "does not implicitly find dependencies" do
build_c_dynlib(compiler_datapath("loader", "bar.c"))
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_library?("bar").should be_true
loader.find_symbol?("bar").should_not be_nil
loader.find_symbol?("foo").should be_nil
Expand All @@ -83,23 +83,23 @@ describe Crystal::Loader do
it "lookup in order" do
build_c_dynlib(compiler_datapath("loader", "foo2.c"))

help_loader1 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
help_loader1 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
help_loader1.load_library?("foo").should be_true
foo_address = help_loader1.find_symbol?("foo").should_not be_nil

help_loader2 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
help_loader2 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
help_loader2.load_library?("foo2").should be_true
foo2_address = help_loader2.find_symbol?("foo").should_not be_nil

foo_address.should_not eq foo2_address

loader1 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader1 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader1.load_library("foo")
loader1.load_library("foo2")

loader1.find_symbol?("foo").should eq foo_address

loader2 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader2 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader2.load_library("foo2")
loader2.load_library("foo")

Expand All @@ -114,18 +114,59 @@ describe Crystal::Loader do
end

it "does not find global symbols" do
loader = Crystal::Loader.new([] of String)
loader = Crystal::Loader.new([] of String, dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.find_symbol?("__crystal_main").should be_nil
end

it "validate that lib handles are properly closed" do
loader = Crystal::Loader.new([] of String)
loader = Crystal::Loader.new([] of String, dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
expect_raises(Crystal::Loader::LoadError, "undefined reference to `foo'") do
loader.find_symbol("foo")
end
end
end

describe "dll_search_paths" do
it "supports an arbitrary path different from lib search path" do
with_tempfile("loader-dll_search_paths") do |path|
FileUtils.mkdir_p(SPEC_CRYSTAL_LOADER_LIB_PATH)
FileUtils.mkdir_p(path)

build_c_dynlib(compiler_datapath("loader", "foo.c"))
File.rename(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, "foo.dll"), File.join(path, "foo.dll"))

loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String, dll_search_paths: [path])
loader.load_library?("foo").should be_true
ensure
loader.try &.close_all

FileUtils.rm_rf(path)
FileUtils.rm_rf(SPEC_CRYSTAL_LOADER_LIB_PATH)
end
end

it "doesn't load DLLs outside dll_search_path or Windows' default search paths" do
with_tempfile("loader-dll_search_paths") do |path|
FileUtils.mkdir_p(SPEC_CRYSTAL_LOADER_LIB_PATH)
FileUtils.mkdir_p(path)

build_c_dynlib(compiler_datapath("loader", "foo.c"))
File.rename(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, "foo.dll"), File.join(path, "foo.dll"))

loader1 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String, dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader1.load_library?("foo").should be_false
loader2 = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader2.load_library?("foo").should be_false
ensure
loader2.try &.close_all
loader1.try &.close_all

FileUtils.rm_rf(path)
FileUtils.rm_rf(SPEC_CRYSTAL_LOADER_LIB_PATH)
end
end
end

describe "lib suffix" do
before_all do
FileUtils.mkdir_p(SPEC_CRYSTAL_LOADER_LIB_PATH)
Expand All @@ -137,15 +178,15 @@ describe Crystal::Loader do

it "respects -dynamic" do
build_c_dynlib(compiler_datapath("loader", "foo.c"), lib_name: "foo-dynamic")
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_library?("foo").should be_true
ensure
loader.close_all if loader
end

it "ignores -static" do
build_c_dynlib(compiler_datapath("loader", "foo.c"), lib_name: "bar-static")
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH], dll_search_paths: [SPEC_CRYSTAL_LOADER_LIB_PATH])
loader.load_library?("bar").should be_false
ensure
loader.close_all if loader
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/codegen/link.cr
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ module Crystal
flags.join(" ")
end

# Searches among CRYSTAL_LIBRARY_PATH, the compiler's directory, and PATH
# for every DLL specified in the used `@[Link]` annotations. Yields the
# absolute path and `true` if found, the base name and `false` if not found.
# The directories should match `Crystal::Repl::Context#dll_search_paths`
def each_dll_path(& : String, Bool ->)
executable_path = nil
compiler_origin = nil
Expand Down
23 changes: 22 additions & 1 deletion src/compiler/crystal/interpreter/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ class Crystal::Repl::Context
# (MSVC doesn't seem to have this issue)
args.delete("-lgc")

Crystal::Loader.parse(args).tap do |loader|
Crystal::Loader.parse(args, dll_search_paths: dll_search_paths).tap do |loader|
# FIXME: Part 2: This is a workaround for initial integration of the interpreter:
# We append a handle to the current executable (i.e. the compiler program)
# to the loader's handle list. This gives the loader access to all the symbols in the compiler program,
Expand All @@ -427,6 +427,27 @@ class Crystal::Repl::Context
end
}

# Extra DLL search paths to mimic compiled code's DLL-copying behavior
# regarding `@[Link]` annotations. These directories should match the ones
# used in `Crystal::Program#each_dll_path`
private def dll_search_paths
{% if flag?(:msvc) %}
paths = CrystalLibraryPath.paths

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)
loader.find_symbol(name)
end
Expand Down
19 changes: 9 additions & 10 deletions src/compiler/crystal/loader.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Crystal::Loader
class LoadError < Exception
property args : Array(String)?
property search_paths : Array(String)?
property dll_search_paths : Array(String)?

def message
String.build do |io|
Expand All @@ -26,28 +27,26 @@ class Crystal::Loader
io << "\nSearch path: "
search_paths.join(io, Process::PATH_DELIMITER)
end
if dll_search_paths = @dll_search_paths
io << "\nDLL search path: "
dll_search_paths.join(io, Process::PATH_DELIMITER)
end
end
end
end

def self.new(search_paths : Array(String), libnames : Array(String), file_paths : Array(String)) : self
loader = new(search_paths)

def load_all(libnames : Array(String), file_paths : Array(String))
file_paths.each do |path|
loader.load_file(::Path[path].expand)
load_file(::Path[path].expand)
end
libnames.each do |libname|
loader.load_library(libname)
load_library(libname)
end
loader
end

getter search_paths : Array(String)
getter loaded_libraries = [] of String

def initialize(@search_paths : Array(String))
@handles = [] of Handle
end
@handles = [] of Handle

# def self.library_filename(libname : String) : String
# raise NotImplementedError.new("library_filename")
Expand Down
Loading