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

Add Crystal::Loader #11434

Merged
merged 6 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions spec/compiler/data/loader/bar.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
int foo();

int bar() {
return foo() + 100;
}
3 changes: 3 additions & 0 deletions spec/compiler/data/loader/foo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
int foo() {
return 12;
}
5 changes: 5 additions & 0 deletions spec/compiler/data/loader/ld.so/basic.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# comment

foo/bar

baz/qux
1 change: 1 addition & 0 deletions spec/compiler/data/loader/ld.so/basic2.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foobar
3 changes: 3 additions & 0 deletions spec/compiler/data/loader/ld.so/include.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include/before
include basic*.conf
include/after
14 changes: 14 additions & 0 deletions spec/compiler/loader/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require "spec"

SPEC_CRYSTAL_LOADER_LIB_PATH = File.join(SPEC_TEMPFILE_PATH, "loader")

def build_c_dynlib(c_filename, target_dir = SPEC_CRYSTAL_LOADER_LIB_PATH)
obj_ext = Crystal::Loader::SHARED_LIBRARY_EXTENSION
o_filename = File.join(target_dir, "lib#{File.basename(c_filename).rchop(".c")}#{obj_ext}")

{% if flag?(:msvc) %}
`cl.exe /nologo /LD #{Process.quote(c_filename)} #{Process.quote("/Fo#{o_filename}")}`.should be_truthy
{% else %}
`#{ENV["CC"]? || "cc"} -shared #{Process.quote(c_filename)} -o #{Process.quote(o_filename)}`.should be_truthy
{% end %}
end
150 changes: 150 additions & 0 deletions spec/compiler/loader/unix_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
{% skip_file unless flag?(:unix) %}

require "./spec_helper"
require "../spec_helper"
require "../../support/env"
require "compiler/crystal/loader"

describe Crystal::Loader do
describe ".parse" do
it "parses directory paths" do
loader = Crystal::Loader.parse(["-L", "/foo/bar/baz", "--library-path", "qux"], search_paths: [] of String)
loader.search_paths.should eq ["/foo/bar/baz", "qux"]
end

it "parses static" do
expect_raises(Crystal::Loader::LoadError, "static libraries are not supported by Crystal's runtime loader") do
Crystal::Loader.parse(["-static"], search_paths: [] of String)
end
end

it "parses library names" do
expect_raises(Crystal::Loader::LoadError, "cannot find -lfoobar") do
Crystal::Loader.parse(["-l", "foobar"], search_paths: [] of String)
end
expect_raises(Crystal::Loader::LoadError, "cannot find -lfoobar") do
Crystal::Loader.parse(["--library", "foobar"], search_paths: [] of String)
end
end

it "parses file paths" do
expect_raises(Crystal::Loader::LoadError, /#{Dir.current}\/foobar\.o.+(No such file or directory|image not found)/) do
Crystal::Loader.parse(["foobar.o"], search_paths: [] of String)
end
expect_raises(Crystal::Loader::LoadError, /#{Dir.current}\/foo\/bar\.o.+(No such file or directory|image not found)/) do
Crystal::Loader.parse(["-l", "foo/bar.o"], search_paths: [] of String)
end
end
end

describe ".default_search_paths" do
it "LD_LIBRARY_PATH" do
with_env "LD_LIBRARY_PATH": "ld1::ld2", "DYLD_LIBRARY_PATH": nil do
search_paths = Crystal::Loader.default_search_paths
{% if flag?(:darwin) %}
search_paths.should eq ["/usr/lib", "/usr/local/lib"]
{% else %}
search_paths[0, 2].should eq ["ld1", "ld2"]
search_paths[-2..].should eq ["/lib", "/usr/lib"]
{% end %}
end
end

it "DYLD_LIBRARY_PATH" do
with_env "DYLD_LIBRARY_PATH": "ld1::ld2", "LD_LIBRARY_PATH": nil do
search_paths = Crystal::Loader.default_search_paths
{% if flag?(:darwin) %}
search_paths[0, 2].should eq ["ld1", "ld2"]
search_paths[-2..].should eq ["/usr/lib", "/usr/local/lib"]
{% else %}
search_paths[-2..].should eq ["/lib", "/usr/lib"]
{% end %}
end
end
end

describe ".read_ld_conf" do
it "basic" do
ary = [] of String
Crystal::Loader.read_ld_conf(ary, compiler_datapath("loader/ld.so/basic.conf"))
ary.should eq ["foo/bar", "baz/qux"]
end

it "with include" do
ary = [] of String
Crystal::Loader.read_ld_conf(ary, compiler_datapath("loader/ld.so/include.conf"))
ary[0].should eq "include/before"
ary[-1].should eq "include/after"
# the order between basic.conf and basic2.conf is system-dependent
ary[1..-2].sort.should eq ["baz/qux", "foo/bar", "foobar"]
end
end

describe "dynlib" do
before_all do
FileUtils.mkdir_p(SPEC_CRYSTAL_LOADER_LIB_PATH)
build_c_dynlib(compiler_datapath("loader", "foo.c"))
end

after_all do
FileUtils.rm_rf(SPEC_CRYSTAL_LOADER_LIB_PATH)
end

describe "#load_file" do
it "finds function symbol" do
loader = Crystal::Loader.new([] of String)
lib_handle = loader.load_file(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, "libfoo#{Crystal::Loader::SHARED_LIBRARY_EXTENSION}"))
lib_handle.should_not be_nil
loader.find_symbol?("foo").should_not be_nil
ensure
loader.close_all if loader
end
end

describe "#load_library" do
it "library name" do
loader = Crystal::Loader.new([SPEC_CRYSTAL_LOADER_LIB_PATH] of String)
lib_handle = loader.load_library("foo")
lib_handle.should_not be_nil
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)
lib_handle = loader.load_library(File.join(SPEC_CRYSTAL_LOADER_LIB_PATH, "libfoo#{Crystal::Loader::SHARED_LIBRARY_EXTENSION}"))
lib_handle.should_not be_nil
loader.find_symbol?("foo").should_not be_nil
ensure
loader.close_all if loader
end

{% unless flag?(:darwin) %}
# FIXME: bar.c doesn't compile on darwin
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)
lib_handle = loader.load_library("bar")
lib_handle.should_not be_nil
loader.find_symbol?("bar").should_not be_nil
loader.find_symbol?("foo").should be_nil
ensure
loader.close_all if loader
end
{% end %}
end

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

it "validate that lib handles are properly closed" do
loader = Crystal::Loader.new([] of String)
expect_raises(Crystal::Loader::LoadError, "undefined reference to `foo'") do
loader.find_symbol("foo")
end
end
end
end
4 changes: 2 additions & 2 deletions spec/support/tempfile.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ def with_temp_executable(name, file = __FILE__)
end
end

def with_temp_c_object_file(c_code, file = __FILE__)
def with_temp_c_object_file(c_code, *, filename = "temp_c", file = __FILE__)
obj_ext = {{ flag?(:win32) ? ".obj" : ".o" }}
with_tempfile("temp_c.c", "temp_c#{obj_ext}", file: file) do |c_filename, o_filename|
with_tempfile("#{filename}.c", "#{filename}#{obj_ext}", file: file) do |c_filename, o_filename|
File.write(c_filename, c_code)

{% if flag?(:win32) %}
Expand Down
115 changes: 115 additions & 0 deletions src/compiler/crystal/loader.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require "option_parser"

# This loader component imitates the behaviour of `ld.so` for linking and loading
# dynamic libraries at runtime.
#
# It provides a tool for interpreted mode, where the compiler does not generate
# an object file that could be passed to the linker. Instead, `Crystal::Loader`
# takes over the job of discovering libraries, loading them into memory and
# 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
end

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

file_paths.each do |path|
loader.load_file(::Path[path].expand)
end
libnames.each do |libname|
loader.load_library(libname)
end
loader
end

getter search_paths : Array(String)

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

# def find_symbol?(name : String) : Handle?
# raise NotImplementedError.new("find_symbol?")
# end

# def load_file(path : String | ::Path) : Handle
# raise NotImplementedError.new("load_file")
# end

# private def open_library(path : String) : Handle
# raise NotImplementedError.new("open_library")
# end

# def self.default_search_paths : Array(String)
# raise NotImplementedError.new("close_all")
# end
Comment on lines +36 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented code should be removed, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it there to define the interface that is expected to be implemented in the platform-specific files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do the same thing in with platform-specific implementations in Crystal::System.


def find_symbol(name : String) : Handle
find_symbol?(name) || raise LoadError.new "undefined reference to `#{name}'"
end

def load_library(libname : String) : Handle
load_library?(libname) || raise LoadError.new "cannot find -l#{libname}"
end

def load_library?(libname : String) : Handle?
if ::Path::SEPARATORS.any? { |separator| libname.includes?(separator) }
return load_file(::Path[libname].expand)
end

find_library_path(libname) do |library_path|
handle = load_file?(library_path)
return handle if handle
end

nil
end

def load_file?(path : String | ::Path) : Handle?
handle = open_library(path.to_s)
return nil unless handle

@handles << handle
handle
end

private def find_library_path(libname)
each_library_path(libname) do |path|
if File.exists?(path)
yield path
end
Comment on lines +83 to +85
Copy link
Contributor

@Sija Sija Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small stylistic suggestion:

Suggested change
if File.exists?(path)
yield path
end
yield path if File.exists?(path)

end
end

private def each_library_path(libname)
@search_paths.each do |directory|
yield "#{directory}/lib#{libname}#{SHARED_LIBRARY_EXTENSION}"
end
end

def close_all : Nil
end

def finalize
close_all
end

SHARED_LIBRARY_EXTENSION = {% if flag?(:darwin) %}
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
".dylib"
{% elsif flag?(:unix) %}
".so"
{% elsif flag?(:windows) %}
".dll"
{% else %}
{% raise "Can't load dynamic libraries" %}
{% end %}
end

{% if flag?(:unix) %}
require "./loader/unix"
{% end %}
Loading