Skip to content

Commit 4fd68e8

Browse files
authored
Add Libdl.LazyLibrary (#50074)
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.
1 parent 43a14f8 commit 4fd68e8

File tree

19 files changed

+321
-60
lines changed

19 files changed

+321
-60
lines changed

Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/jul
8484
julia-libccalltest: julia-deps
8585
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalltest
8686

87+
julia-libccalllazyfoo: julia-deps
88+
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalllazyfoo
89+
90+
julia-libccalllazybar: julia-deps julia-libccalllazyfoo
91+
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalllazybar
92+
8793
julia-libllvmcalltest: julia-deps
8894
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libllvmcalltest
8995

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

105-
julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest julia-libllvmcalltest julia-base-cache
111+
julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest \
112+
julia-libccalllazyfoo julia-libccalllazybar julia-libllvmcalltest julia-base-cache
106113

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

191198
# private libraries, that are installed in $(prefix)/lib/julia
192-
JL_PRIVATE_LIBS-0 := libccalltest libllvmcalltest
199+
JL_PRIVATE_LIBS-0 := libccalltest libccalllazyfoo libccalllazybar libllvmcalltest
193200
ifeq ($(JULIA_BUILD_MODE),release)
194201
JL_PRIVATE_LIBS-0 += libjulia-internal libjulia-codegen
195202
else ifeq ($(JULIA_BUILD_MODE),debug)

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Language changes
1010
Compiler/Runtime improvements
1111
-----------------------------
1212
* Updated GC heuristics to count allocated pages instead of individual objects ([#50144]).
13+
* A new `LazyLibrary` type is exported from `Libdl` for use in building chained lazy library
14+
loads, primarily to be used within JLLs ([#50074]).
1315

1416
Command-line option changes
1517
---------------------------

base/Base.jl

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ include("missing.jl")
314314
# version
315315
include("version.jl")
316316

317+
# Concurrency (part 1)
318+
include("linked_list.jl")
319+
include("condition.jl")
320+
include("threads.jl")
321+
include("lock.jl")
322+
317323
# system & environment
318324
include("sysinfo.jl")
319325
include("libc.jl")
@@ -328,11 +334,9 @@ const liblapack_name = libblas_name
328334
include("logging.jl")
329335
using .CoreLogging
330336

331-
# Concurrency
332-
include("linked_list.jl")
333-
include("condition.jl")
334-
include("threads.jl")
335-
include("lock.jl")
337+
# Concurrency (part 2)
338+
# Note that `atomics.jl` here should be deprecated
339+
Core.eval(Threads, :(include("atomics.jl")))
336340
include("channels.jl")
337341
include("partr.jl")
338342
include("task.jl")

base/libdl.jl

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Base.DL_LOAD_PATH
99

1010
export DL_LOAD_PATH, RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL,
1111
RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW, dlclose, dlopen, dlopen_e, dlsym, dlsym_e,
12-
dlpath, find_library, dlext, dllist
12+
dlpath, find_library, dlext, dllist, LazyLibrary, LazyLibraryPath, BundledLazyLibraryPath
1313

1414
"""
1515
DL_LOAD_PATH
@@ -45,6 +45,9 @@ applicable.
4545
"""
4646
(RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL, RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW)
4747

48+
# The default flags for `dlopen()`
49+
const default_rtld_flags = RTLD_LAZY | RTLD_DEEPBIND
50+
4851
"""
4952
dlsym(handle, sym; throw_error::Bool = true)
5053
@@ -72,8 +75,8 @@ end
7275
Look up a symbol from a shared library handle, silently return `C_NULL` on lookup failure.
7376
This method is now deprecated in favor of `dlsym(handle, sym; throw_error=false)`.
7477
"""
75-
function dlsym_e(hnd::Ptr, s::Union{Symbol,AbstractString})
76-
return something(dlsym(hnd, s; throw_error=false), C_NULL)
78+
function dlsym_e(args...)
79+
return something(dlsym(args...; throw_error=false), C_NULL)
7780
end
7881

7982
"""
@@ -110,10 +113,10 @@ If the library cannot be found, this method throws an error, unless the keyword
110113
"""
111114
function dlopen end
112115

113-
dlopen(s::Symbol, flags::Integer = RTLD_LAZY | RTLD_DEEPBIND; kwargs...) =
116+
dlopen(s::Symbol, flags::Integer = default_rtld_flags; kwargs...) =
114117
dlopen(string(s), flags; kwargs...)
115118

116-
function dlopen(s::AbstractString, flags::Integer = RTLD_LAZY | RTLD_DEEPBIND; throw_error::Bool = true)
119+
function dlopen(s::AbstractString, flags::Integer = default_rtld_flags; throw_error::Bool = true)
117120
ret = ccall(:jl_load_dynamic_library, Ptr{Cvoid}, (Cstring,UInt32,Cint), s, flags, Cint(throw_error))
118121
if ret == C_NULL
119122
return nothing
@@ -138,10 +141,10 @@ vendor = dlopen("libblas") do lib
138141
end
139142
```
140143
"""
141-
function dlopen(f::Function, args...; kwargs...)
144+
function dlopen(f::Function, name, args...; kwargs...)
142145
hdl = nothing
143146
try
144-
hdl = dlopen(args...; kwargs...)
147+
hdl = dlopen(name, args...; kwargs...)
145148
f(hdl)
146149
finally
147150
dlclose(hdl)
@@ -314,4 +317,135 @@ function dllist()
314317
return dynamic_libraries
315318
end
316319

320+
321+
"""
322+
LazyLibraryPath
323+
324+
Helper type for lazily constructed library paths for use with `LazyLibrary`.
325+
Arguments are passed to `joinpath()`. Arguments must be able to have
326+
`string()` called on them.
327+
328+
```
329+
libfoo = LazyLibrary(LazyLibraryPath(prefix, "lib/libfoo.so.1.2.3"))
330+
```
331+
"""
332+
struct LazyLibraryPath
333+
pieces::Vector
334+
LazyLibraryPath(pieces::Vector) = new(pieces)
335+
end
336+
LazyLibraryPath(args...) = LazyLibraryPath(collect(args))
337+
Base.string(llp::LazyLibraryPath) = joinpath(string.(llp.pieces)...)
338+
Base.cconvert(::Type{Cstring}, llp::LazyLibraryPath) = Base.cconvert(Cstring, string(llp))
339+
# Define `print` so that we can wrap this in a `LazyString`
340+
Base.print(io::IO, llp::LazyLibraryPath) = print(io, string(llp))
341+
342+
# Helper to get `Sys.BINDIR` at runtime
343+
struct SysBindirGetter; end
344+
Base.string(::SysBindirGetter) = dirname(Sys.BINDIR)
345+
346+
"""
347+
BundledLazyLibraryPath
348+
349+
Helper type for lazily constructed library paths that are stored within the
350+
bundled Julia distribution, primarily for use by Base modules.
351+
352+
```
353+
libfoo = LazyLibrary(BundledLazyLibraryPath("lib/libfoo.so.1.2.3"))
354+
```
355+
"""
356+
BundledLazyLibraryPath(subpath) = LazyLibraryPath(SysBindirGetter(), subpath)
357+
358+
359+
"""
360+
LazyLibrary(name, flags = <default dlopen flags>,
361+
dependencies = LazyLibrary[], on_load_callback = nothing)
362+
363+
Represents a lazily-loaded library that opens itself and its dependencies on first usage
364+
in a `dlopen()`, `dlsym()`, or `ccall()` usage. While this structure contains the
365+
ability to run arbitrary code on first load via `on_load_callback`, we caution that this
366+
should be used sparingly, as it is not expected that `ccall()` should result in large
367+
amounts of Julia code being run. You may call `ccall()` from within the
368+
`on_load_callback` but only for the current library and its dependencies, and user should
369+
not call `wait()` on any tasks within the on load callback.
370+
"""
371+
mutable struct LazyLibrary
372+
# Name and flags to open with
373+
const path
374+
const flags::UInt32
375+
376+
# Dependencies that must be loaded before we can load
377+
dependencies::Vector{LazyLibrary}
378+
379+
# Function that get called once upon initial load
380+
on_load_callback
381+
const lock::Base.ReentrantLock
382+
383+
# Pointer that we eventually fill out upon first `dlopen()`
384+
@atomic handle::Ptr{Cvoid}
385+
function LazyLibrary(path; flags = default_rtld_flags, dependencies = LazyLibrary[],
386+
on_load_callback = nothing)
387+
return new(
388+
path,
389+
UInt32(flags),
390+
collect(dependencies),
391+
on_load_callback,
392+
Base.ReentrantLock(),
393+
C_NULL,
394+
)
395+
end
396+
end
397+
398+
# We support adding dependencies only because of very special situations
399+
# such as LBT needing to have OpenBLAS_jll added as a dependency dynamically.
400+
function add_dependency!(ll::LazyLibrary, dep::LazyLibrary)
401+
@lock ll.lock begin
402+
push!(ll.dependencies, dep)
403+
end
404+
end
405+
406+
# Register `jl_libdl_dlopen_func` so that `ccall()` lowering knows
407+
# how to call `dlopen()`, during bootstrap.
408+
# See `post_image_load_hooks` for non-bootstrapping.
409+
Base.unsafe_store!(cglobal(:jl_libdl_dlopen_func, Any), dlopen)
410+
411+
function dlopen(ll::LazyLibrary, flags::Integer = ll.flags; kwargs...)
412+
handle = @atomic :acquire ll.handle
413+
if handle == C_NULL
414+
@lock ll.lock begin
415+
# Check to see if another thread has already run this
416+
if ll.handle == C_NULL
417+
# Ensure that all dependencies are loaded
418+
for dep in ll.dependencies
419+
dlopen(dep; kwargs...)
420+
end
421+
422+
# Load our library
423+
handle = dlopen(string(ll.path), flags; kwargs...)
424+
@atomic :release ll.handle = handle
425+
426+
# Only the thread that loaded the library calls the `on_load_callback()`.
427+
if ll.on_load_callback !== nothing
428+
ll.on_load_callback()
429+
end
430+
end
431+
end
432+
else
433+
# Invoke our on load callback, if it exists
434+
if ll.on_load_callback !== nothing
435+
# This empty lock protects against the case where we have updated
436+
# `ll.handle` in the branch above, but not exited the lock. We want
437+
# a second thread that comes in at just the wrong time to have to wait
438+
# for that lock to be released (and thus for the on_load_callback to
439+
# have finished), hence the empty lock here. But we want the
440+
# on_load_callback thread to bypass this, which will be happen thanks
441+
# to the fact that we're using a reentrant lock here.
442+
@lock ll.lock begin end
443+
end
444+
end
445+
446+
return handle
447+
end
448+
dlopen(x::Any) = throw(TypeError(:dlopen, "", Union{Symbol,String,LazyLibrary}, x))
449+
dlsym(ll::LazyLibrary, args...; kwargs...) = dlsym(dlopen(ll), args...; kwargs...)
450+
dlpath(ll::LazyLibrary) = dlpath(dlopen(ll))
317451
end # module Libdl

base/threads.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ module Threads
88
global Condition # we'll define this later, make sure we don't import Base.Condition
99

1010
include("threadingconstructs.jl")
11-
include("atomics.jl")
1211
include("locks-mt.jl")
1312

1413
end

doc/src/devdocs/locks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ may result in pernicious and hard-to-find deadlocks. BE VERY CAREFUL!
9191
>
9292
> > this may continue to be held after releasing the iolock, or acquired without it,
9393
> > but be very careful to never attempt to acquire the iolock while holding it
94+
>
95+
> * Libdl.LazyLibrary lock
9496
9597

9698
The following is the root lock, meaning no other lock shall be held when trying to acquire it:

src/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ $(build_includedir)/julia/uv/*.h: $(LIBUV_INC)/uv/*.h | $(build_includedir)/juli
252252
$(INSTALL_F) $^ $(build_includedir)/julia/uv
253253

254254
libccalltest: $(build_shlibdir)/libccalltest.$(SHLIB_EXT)
255+
libccalllazyfoo: $(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT)
256+
libccalllazybar: $(build_shlibdir)/libccalllazybar.$(SHLIB_EXT)
255257
libllvmcalltest: $(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT)
256258

257259
ifeq ($(OS), Linux)
@@ -276,6 +278,12 @@ endif
276278
mv $@.tmp $@
277279
$(INSTALL_NAME_CMD)libccalltest.$(SHLIB_EXT) $@
278280

281+
$(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT): $(SRCDIR)/ccalllazyfoo.c
282+
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(call SONAME_FLAGS,ccalllazyfoo.$(SHLIB_EXT)))
283+
284+
$(build_shlibdir)/libccalllazybar.$(SHLIB_EXT): $(SRCDIR)/ccalllazybar.c $(build_shlibdir)/libccalllazyfoo.$(SHLIB_EXT)
285+
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(call SONAME_FLAGS,ccalllazybar.$(SHLIB_EXT)) -lccalllazyfoo)
286+
279287
$(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT): $(SRCDIR)/llvmcalltest.cpp $(LLVM_CONFIG_ABSOLUTE)
280288
@$(call PRINT_CC, $(CXX) $(LLVM_CXXFLAGS) $(FLAGS) $(CPPFLAGS) $(CXXFLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(NO_WHOLE_ARCHIVE) $(CG_LLVMLINK)) -lpthread
281289

src/ccall.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,9 @@ static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_va
663663
f_lib = jl_symbol_name((jl_sym_t*)t1);
664664
else if (jl_is_string(t1))
665665
f_lib = jl_string_data(t1);
666-
else
667-
f_name = NULL;
666+
else {
667+
out.lib_expr = t1;
668+
}
668669
}
669670
}
670671
}

src/ccalllazybar.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
#include "ccalltest_common.h"
4+
5+
// We expect this to come from `libccalllazyfoo`
6+
extern int foo(int);
7+
8+
DLLEXPORT int bar(int a) {
9+
return foo(a + 1);
10+
}

src/ccalllazyfoo.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
#include "ccalltest_common.h"
4+
5+
DLLEXPORT int foo(int a) {
6+
return a*2;
7+
}

0 commit comments

Comments
 (0)