Skip to content

Commit

Permalink
Add Libdl.LazyLibrary (#50074)
Browse files Browse the repository at this point in the history
This provides an in-base mechanism to handle chained library
dependencies. In essence, the `LazyLibrary` object can be used anywhere
a pointer to a library can be used (`dlopen`, `dlsym`, `ccall`, etc...)
but it delays loading the library (and its recursive dependencies) until
it is actually needed.

This is the foundational piece needed to upgrade JLLs to lazily-load
their libraries. In this new scheme, JLLs would generally lose all
executable code and consist of nothing more than `LazyLibrary`
definitions.
  • Loading branch information
staticfloat committed Jul 27, 2023
1 parent 43a14f8 commit 4fd68e8
Show file tree
Hide file tree
Showing 19 changed files with 321 additions and 60 deletions.
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/jul
julia-libccalltest: julia-deps
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalltest

julia-libccalllazyfoo: julia-deps
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalllazyfoo

julia-libccalllazybar: julia-deps julia-libccalllazyfoo
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalllazybar

julia-libllvmcalltest: julia-deps
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libllvmcalltest

Expand All @@ -102,7 +108,8 @@ julia-sysimg-bc : julia-stdlib julia-base julia-cli-$(JULIA_BUILD_MODE) julia-sr
julia-sysimg-release julia-sysimg-debug : julia-sysimg-% : julia-sysimg-ji julia-src-%
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f sysimage.mk sysimg-$*

julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest julia-libllvmcalltest julia-base-cache
julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest \
julia-libccalllazyfoo julia-libccalllazybar julia-libllvmcalltest julia-base-cache

stdlibs-cache-release stdlibs-cache-debug : stdlibs-cache-% : julia-%
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f pkgimage.mk all-$*
Expand Down Expand Up @@ -189,7 +196,7 @@ JL_TARGETS := julia-debug
endif

# private libraries, that are installed in $(prefix)/lib/julia
JL_PRIVATE_LIBS-0 := libccalltest libllvmcalltest
JL_PRIVATE_LIBS-0 := libccalltest libccalllazyfoo libccalllazybar libllvmcalltest
ifeq ($(JULIA_BUILD_MODE),release)
JL_PRIVATE_LIBS-0 += libjulia-internal libjulia-codegen
else ifeq ($(JULIA_BUILD_MODE),debug)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Language changes
Compiler/Runtime improvements
-----------------------------
* Updated GC heuristics to count allocated pages instead of individual objects ([#50144]).
* A new `LazyLibrary` type is exported from `Libdl` for use in building chained lazy library
loads, primarily to be used within JLLs ([#50074]).

Command-line option changes
---------------------------
Expand Down
14 changes: 9 additions & 5 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ include("missing.jl")
# version
include("version.jl")

# Concurrency (part 1)
include("linked_list.jl")
include("condition.jl")
include("threads.jl")
include("lock.jl")

# system & environment
include("sysinfo.jl")
include("libc.jl")
Expand All @@ -328,11 +334,9 @@ const liblapack_name = libblas_name
include("logging.jl")
using .CoreLogging

# Concurrency
include("linked_list.jl")
include("condition.jl")
include("threads.jl")
include("lock.jl")
# Concurrency (part 2)
# Note that `atomics.jl` here should be deprecated
Core.eval(Threads, :(include("atomics.jl")))
include("channels.jl")
include("partr.jl")
include("task.jl")
Expand Down
148 changes: 141 additions & 7 deletions base/libdl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Base.DL_LOAD_PATH

export DL_LOAD_PATH, RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL,
RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW, dlclose, dlopen, dlopen_e, dlsym, dlsym_e,
dlpath, find_library, dlext, dllist
dlpath, find_library, dlext, dllist, LazyLibrary, LazyLibraryPath, BundledLazyLibraryPath

"""
DL_LOAD_PATH
Expand Down Expand Up @@ -45,6 +45,9 @@ applicable.
"""
(RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL, RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW)

# The default flags for `dlopen()`
const default_rtld_flags = RTLD_LAZY | RTLD_DEEPBIND

"""
dlsym(handle, sym; throw_error::Bool = true)
Expand Down Expand Up @@ -72,8 +75,8 @@ end
Look up a symbol from a shared library handle, silently return `C_NULL` on lookup failure.
This method is now deprecated in favor of `dlsym(handle, sym; throw_error=false)`.
"""
function dlsym_e(hnd::Ptr, s::Union{Symbol,AbstractString})
return something(dlsym(hnd, s; throw_error=false), C_NULL)
function dlsym_e(args...)
return something(dlsym(args...; throw_error=false), C_NULL)
end

"""
Expand Down Expand Up @@ -110,10 +113,10 @@ If the library cannot be found, this method throws an error, unless the keyword
"""
function dlopen end

dlopen(s::Symbol, flags::Integer = RTLD_LAZY | RTLD_DEEPBIND; kwargs...) =
dlopen(s::Symbol, flags::Integer = default_rtld_flags; kwargs...) =
dlopen(string(s), flags; kwargs...)

function dlopen(s::AbstractString, flags::Integer = RTLD_LAZY | RTLD_DEEPBIND; throw_error::Bool = true)
function dlopen(s::AbstractString, flags::Integer = default_rtld_flags; throw_error::Bool = true)
ret = ccall(:jl_load_dynamic_library, Ptr{Cvoid}, (Cstring,UInt32,Cint), s, flags, Cint(throw_error))
if ret == C_NULL
return nothing
Expand All @@ -138,10 +141,10 @@ vendor = dlopen("libblas") do lib
end
```
"""
function dlopen(f::Function, args...; kwargs...)
function dlopen(f::Function, name, args...; kwargs...)
hdl = nothing
try
hdl = dlopen(args...; kwargs...)
hdl = dlopen(name, args...; kwargs...)
f(hdl)
finally
dlclose(hdl)
Expand Down Expand Up @@ -314,4 +317,135 @@ function dllist()
return dynamic_libraries
end


"""
LazyLibraryPath
Helper type for lazily constructed library paths for use with `LazyLibrary`.
Arguments are passed to `joinpath()`. Arguments must be able to have
`string()` called on them.
```
libfoo = LazyLibrary(LazyLibraryPath(prefix, "lib/libfoo.so.1.2.3"))
```
"""
struct LazyLibraryPath
pieces::Vector
LazyLibraryPath(pieces::Vector) = new(pieces)
end
LazyLibraryPath(args...) = LazyLibraryPath(collect(args))
Base.string(llp::LazyLibraryPath) = joinpath(string.(llp.pieces)...)
Base.cconvert(::Type{Cstring}, llp::LazyLibraryPath) = Base.cconvert(Cstring, string(llp))
# Define `print` so that we can wrap this in a `LazyString`
Base.print(io::IO, llp::LazyLibraryPath) = print(io, string(llp))

# Helper to get `Sys.BINDIR` at runtime
struct SysBindirGetter; end
Base.string(::SysBindirGetter) = dirname(Sys.BINDIR)

"""
BundledLazyLibraryPath
Helper type for lazily constructed library paths that are stored within the
bundled Julia distribution, primarily for use by Base modules.
```
libfoo = LazyLibrary(BundledLazyLibraryPath("lib/libfoo.so.1.2.3"))
```
"""
BundledLazyLibraryPath(subpath) = LazyLibraryPath(SysBindirGetter(), subpath)


"""
LazyLibrary(name, flags = <default dlopen flags>,
dependencies = LazyLibrary[], on_load_callback = nothing)
Represents a lazily-loaded library that opens itself and its dependencies on first usage
in a `dlopen()`, `dlsym()`, or `ccall()` usage. While this structure contains the
ability to run arbitrary code on first load via `on_load_callback`, we caution that this
should be used sparingly, as it is not expected that `ccall()` should result in large
amounts of Julia code being run. You may call `ccall()` from within the
`on_load_callback` but only for the current library and its dependencies, and user should
not call `wait()` on any tasks within the on load callback.
"""
mutable struct LazyLibrary
# Name and flags to open with
const path
const flags::UInt32

# Dependencies that must be loaded before we can load
dependencies::Vector{LazyLibrary}

# Function that get called once upon initial load
on_load_callback
const lock::Base.ReentrantLock

# Pointer that we eventually fill out upon first `dlopen()`
@atomic handle::Ptr{Cvoid}
function LazyLibrary(path; flags = default_rtld_flags, dependencies = LazyLibrary[],
on_load_callback = nothing)
return new(
path,
UInt32(flags),
collect(dependencies),
on_load_callback,
Base.ReentrantLock(),
C_NULL,
)
end
end

# We support adding dependencies only because of very special situations
# such as LBT needing to have OpenBLAS_jll added as a dependency dynamically.
function add_dependency!(ll::LazyLibrary, dep::LazyLibrary)
@lock ll.lock begin
push!(ll.dependencies, dep)
end
end

# Register `jl_libdl_dlopen_func` so that `ccall()` lowering knows
# how to call `dlopen()`, during bootstrap.
# See `post_image_load_hooks` for non-bootstrapping.
Base.unsafe_store!(cglobal(:jl_libdl_dlopen_func, Any), dlopen)

function dlopen(ll::LazyLibrary, flags::Integer = ll.flags; kwargs...)
handle = @atomic :acquire ll.handle
if handle == C_NULL
@lock ll.lock begin
# Check to see if another thread has already run this
if ll.handle == C_NULL
# Ensure that all dependencies are loaded
for dep in ll.dependencies
dlopen(dep; kwargs...)
end

# Load our library
handle = dlopen(string(ll.path), flags; kwargs...)
@atomic :release ll.handle = handle

# Only the thread that loaded the library calls the `on_load_callback()`.
if ll.on_load_callback !== nothing
ll.on_load_callback()
end
end
end
else
# Invoke our on load callback, if it exists
if ll.on_load_callback !== nothing
# This empty lock protects against the case where we have updated
# `ll.handle` in the branch above, but not exited the lock. We want
# a second thread that comes in at just the wrong time to have to wait
# for that lock to be released (and thus for the on_load_callback to
# have finished), hence the empty lock here. But we want the
# on_load_callback thread to bypass this, which will be happen thanks
# to the fact that we're using a reentrant lock here.
@lock ll.lock begin end
end
end

return handle
end
dlopen(x::Any) = throw(TypeError(:dlopen, "", Union{Symbol,String,LazyLibrary}, x))
dlsym(ll::LazyLibrary, args...; kwargs...) = dlsym(dlopen(ll), args...; kwargs...)
dlpath(ll::LazyLibrary) = dlpath(dlopen(ll))
end # module Libdl
1 change: 0 additions & 1 deletion base/threads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module Threads
global Condition # we'll define this later, make sure we don't import Base.Condition

include("threadingconstructs.jl")
include("atomics.jl")
include("locks-mt.jl")

end
2 changes: 2 additions & 0 deletions doc/src/devdocs/locks.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ may result in pernicious and hard-to-find deadlocks. BE VERY CAREFUL!
>
> > this may continue to be held after releasing the iolock, or acquired without it,
> > but be very careful to never attempt to acquire the iolock while holding it
>
> * Libdl.LazyLibrary lock

The following is the root lock, meaning no other lock shall be held when trying to acquire it:
Expand Down
8 changes: 8 additions & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ $(build_includedir)/julia/uv/*.h: $(LIBUV_INC)/uv/*.h | $(build_includedir)/juli
$(INSTALL_F) $^ $(build_includedir)/julia/uv

libccalltest: $(build_shlibdir)/libccalltest.$(SHLIB_EXT)
libccalllazyfoo: $(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT)
libccalllazybar: $(build_shlibdir)/libccalllazybar.$(SHLIB_EXT)
libllvmcalltest: $(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT)

ifeq ($(OS), Linux)
Expand All @@ -276,6 +278,12 @@ endif
mv $@.tmp $@
$(INSTALL_NAME_CMD)libccalltest.$(SHLIB_EXT) $@

$(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT): $(SRCDIR)/ccalllazyfoo.c
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(call SONAME_FLAGS,ccalllazyfoo.$(SHLIB_EXT)))

$(build_shlibdir)/libccalllazybar.$(SHLIB_EXT): $(SRCDIR)/ccalllazybar.c $(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT)
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(call SONAME_FLAGS,ccalllazybar.$(SHLIB_EXT)) -lccalllazyfoo)

$(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT): $(SRCDIR)/llvmcalltest.cpp $(LLVM_CONFIG_ABSOLUTE)
@$(call PRINT_CC, $(CXX) $(LLVM_CXXFLAGS) $(FLAGS) $(CPPFLAGS) $(CXXFLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(NO_WHOLE_ARCHIVE) $(CG_LLVMLINK)) -lpthread

Expand Down
5 changes: 3 additions & 2 deletions src/ccall.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,9 @@ static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_va
f_lib = jl_symbol_name((jl_sym_t*)t1);
else if (jl_is_string(t1))
f_lib = jl_string_data(t1);
else
f_name = NULL;
else {
out.lib_expr = t1;
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/ccalllazybar.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is a part of Julia. License is MIT: https://julialang.org/license

#include "ccalltest_common.h"

// We expect this to come from `libccalllazyfoo`
extern int foo(int);

DLLEXPORT int bar(int a) {
return foo(a + 1);
}
7 changes: 7 additions & 0 deletions src/ccalllazyfoo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is a part of Julia. License is MIT: https://julialang.org/license

#include "ccalltest_common.h"

DLLEXPORT int foo(int a) {
return a*2;
}
33 changes: 1 addition & 32 deletions src/ccalltest.c
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
// This file is a part of Julia. License is MIT: https://julialang.org/license

#include <stdio.h>
#include <stdlib.h>
#include <complex.h>
#include <stdint.h>
#include <inttypes.h>

#include "../src/support/platform.h"
#include "../src/support/dtypes.h"

// Borrow definition from `support/dtypes.h`
#ifdef _OS_WINDOWS_
# define DLLEXPORT __declspec(dllexport)
#else
# if defined(_OS_LINUX_) && !defined(_COMPILER_CLANG_)
// Clang and ld disagree about the proper relocation for STV_PROTECTED, causing
// linker errors.
# define DLLEXPORT __attribute__ ((visibility("protected")))
# else
# define DLLEXPORT __attribute__ ((visibility("default")))
# endif
#endif


#ifdef _P64
#define jint int64_t
#define PRIjint PRId64
#else
#define jint int32_t
#define PRIjint PRId32
#endif
#include "ccalltest_common.h"

int verbose = 1;

int c_int = 0;


//////////////////////////////////
// Test for proper argument register truncation

Expand Down
Loading

0 comments on commit 4fd68e8

Please sign in to comment.