Skip to content

Commit 77a5027

Browse files
committed
Add Libdl.LazyLibrary
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 53bcb39 commit 77a5027

File tree

12 files changed

+241
-35
lines changed

12 files changed

+241
-35
lines changed

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ 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-libccalltestdep: julia-deps julia-libccalltest
88+
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libccalltestdep
89+
8790
julia-libllvmcalltest: julia-deps
8891
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/src libllvmcalltest
8992

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

105-
julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest julia-libllvmcalltest julia-base-cache
108+
julia-debug julia-release : julia-% : julia-sysimg-% julia-src-% julia-symlink julia-libccalltest julia-libccalltestdep julia-libllvmcalltest julia-base-cache
106109

107110
stdlibs-cache-release stdlibs-cache-debug : stdlibs-cache-% : julia-%
108111
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT) -f pkgimage.mk all-$*
@@ -189,7 +192,7 @@ JL_TARGETS := julia-debug
189192
endif
190193

191194
# private libraries, that are installed in $(prefix)/lib/julia
192-
JL_PRIVATE_LIBS-0 := libccalltest libllvmcalltest
195+
JL_PRIVATE_LIBS-0 := libccalltest libccalltestdep libllvmcalltest
193196
ifeq ($(JULIA_BUILD_MODE),release)
194197
JL_PRIVATE_LIBS-0 += libjulia-internal libjulia-codegen
195198
else ifeq ($(JULIA_BUILD_MODE),debug)

base/libdl.jl

Lines changed: 105 additions & 22 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
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
@@ -226,23 +229,6 @@ function dlpath(handle::Ptr{Cvoid})
226229
return s
227230
end
228231

229-
"""
230-
dlpath(libname::Union{AbstractString, Symbol})
231-
232-
Get the full path of the library `libname`.
233-
234-
# Example
235-
```julia-repl
236-
julia> dlpath("libjulia")
237-
```
238-
"""
239-
function dlpath(libname::Union{AbstractString, Symbol})
240-
handle = dlopen(libname)
241-
path = dlpath(handle)
242-
dlclose(handle)
243-
return path
244-
end
245-
246232
if Sys.isapple()
247233
const dlext = "dylib"
248234
elseif Sys.iswindows()
@@ -314,4 +300,101 @@ function dllist()
314300
return dynamic_libraries
315301
end
316302

303+
304+
"""
305+
LazyLibrary(name, flags = <default dlopen flags>,
306+
dependencies = LazyLibrary[], on_load_callback = nothing)
307+
308+
Represents a lazily-loaded library that opens itself and its dependencies on first usage
309+
in a `dlopen()`, `dlsym()`, or `ccall()` usage. While this structure contains the
310+
ability to run arbitrary code on first load via `on_load_callback`, we caution that this
311+
should be used sparingly, as it is not expected that `ccall()` should result in large
312+
amounts of Julia code being run.
313+
"""
314+
mutable struct LazyLibrary
315+
# Name and flags to open with
316+
const name::String
317+
const flags::UInt32
318+
319+
# Dependencies that must be loaded before we can load
320+
const dependencies::Vector{LazyLibrary}
321+
322+
# Function that gets called once upon initial load with the pointer as an argument
323+
const on_load_callback::Union{Nothing,Function}
324+
# Function that gets called once upon final unload with the pointer as an argument
325+
const on_unload_callback::Union{Nothing,Function}
326+
327+
# Pointer that we eventually fill out upon first `dlopen()`
328+
@atomic handle::Ptr{Cvoid}
329+
@atomic refs::Int
330+
function LazyLibrary(name, flags = default_rtld_flags, dependencies = LazyLibrary[],
331+
on_load_callback = nothing, on_unload_callback = nothing)
332+
return new(
333+
String(name),
334+
UInt32(flags),
335+
collect(dependencies),
336+
on_load_callback,
337+
on_unload_callback,
338+
C_NULL,
339+
0,
340+
)
341+
end
342+
end
343+
344+
function dlopen(ll::LazyLibrary, flags::Integer = ll.flags; kwargs...)
345+
# Only load once
346+
handle = @atomic ll.handle
347+
if handle != C_NULL
348+
@atomic ll.refs += 1
349+
return handle
350+
end
351+
352+
# Ensure that all dependencies are loaded
353+
for dep in ll.dependencies
354+
dlopen(dep; kwargs...)
355+
end
356+
357+
# Load our library
358+
handle = dlopen(ll.name, flags; kwargs...)
359+
@atomic ll.handle = handle
360+
@atomic ll.refs += 1
361+
362+
# Invoke our on load callback, if it exists
363+
if ll.on_load_callback !== nothing
364+
ll.on_load_callback(handle)
365+
end
366+
return handle
367+
end
368+
dlsym(ll::LazyLibrary, args...; kwargs...) = dlsym(dlopen(ll), args...; kwargs...)
369+
function dlclose(ll::LazyLibrary)
370+
if @atomic(ll.refs) > 0 && @atomic(ll.handle) != C_NULL
371+
@atomic ll.refs -= 1
372+
if @atomic(ll.refs) <= 0
373+
if ll.on_unload_callback !== nothing
374+
ll.on_unload_callback(@atomic(ll.handle))
375+
end
376+
dlclose(@atomic(ll.handle))
377+
@atomic ll.handle = C_NULL
378+
end
379+
end
380+
end
381+
382+
"""
383+
dlpath(libname)
384+
385+
Get the full path of the library `libname`. `libname` can be a string, a symbol
386+
or a `LazyLibrary` object.
387+
388+
# Example
389+
```julia-repl
390+
julia> dlpath("libjulia")
391+
```
392+
"""
393+
function dlpath(libname::Union{AbstractString, Symbol, LazyLibrary})
394+
handle = dlopen(libname)
395+
path = dlpath(handle)
396+
dlclose(handle)
397+
return path
398+
end
399+
317400
end # module Libdl

src/Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ $(build_includedir)/julia/uv/*.h: $(LIBUV_INC)/uv/*.h | $(build_includedir)/juli
249249
$(INSTALL_F) $^ $(build_includedir)/julia/uv
250250

251251
libccalltest: $(build_shlibdir)/libccalltest.$(SHLIB_EXT)
252+
libccalltestdep: $(build_shlibdir)/libccalltestdep.$(SHLIB_EXT)
252253
libllvmcalltest: $(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT)
253254

254255
ifeq ($(OS), Linux)
@@ -257,7 +258,7 @@ else
257258
JULIA_SPLITDEBUG := 0
258259
endif
259260
$(build_shlibdir)/libccalltest.$(SHLIB_EXT): $(SRCDIR)/ccalltest.c
260-
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@.tmp $(LDFLAGS))
261+
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@.tmp $(LDFLAGS) $(call SONAME_FLAGS,libccalltest.$(SHLIB_EXT)))
261262
$(INSTALL_NAME_CMD)libccalltest.$(SHLIB_EXT) $@.tmp
262263
ifeq ($(JULIA_SPLITDEBUG),1)
263264
@# Create split debug info file for libccalltest stacktraces test
@@ -273,6 +274,9 @@ endif
273274
274275
$(INSTALL_NAME_CMD)libccalltest.$(SHLIB_EXT) $@
275276

277+
$(build_shlibdir)/libccalltestdep.$(SHLIB_EXT): $(SRCDIR)/ccalltestdep.c $(build_shlibdir)/libccalltest.$(SHLIB_EXT)
278+
@$(call PRINT_CC, $(CC) $(JCFLAGS) $(JL_CFLAGS) $(JCPPFLAGS) $(FLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(call SONAME_FLAGS,libccalltestdep.$(SHLIB_EXT)) -lccalltest)
279+
276280
$(build_shlibdir)/libllvmcalltest.$(SHLIB_EXT): $(SRCDIR)/llvmcalltest.cpp $(LLVM_CONFIG_ABSOLUTE)
277281
@$(call PRINT_CC, $(CXX) $(LLVM_CXXFLAGS) $(FLAGS) $(CPPFLAGS) $(CXXFLAGS) -O3 $< $(fPIC) -shared -o $@ $(LDFLAGS) $(COMMON_LIBPATHS) $(NO_WHOLE_ARCHIVE) $(CG_LLVMLINK)) -lpthread
278282

src/ccall.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,9 @@ static void interpret_symbol_arg(jl_codectx_t &ctx, native_sym_arg_t &out, jl_va
651651
f_lib = jl_symbol_name((jl_sym_t*)t1);
652652
else if (jl_is_string(t1))
653653
f_lib = jl_string_data(t1);
654+
else if (jl_lazy_library_type != NULL && jl_isa(t1, (jl_value_t *)jl_lazy_library_type)) {
655+
out.lib_expr = t1;
656+
}
654657
else
655658
f_name = NULL;
656659
}

src/ccalltestdep.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
#include <stdio.h>
4+
#include <stdlib.h>
5+
#include <complex.h>
6+
#include <stdint.h>
7+
#include <inttypes.h>
8+
9+
#include "../src/support/platform.h"
10+
#include "../src/support/dtypes.h"
11+
12+
// Borrow definition from `support/dtypes.h`
13+
#ifdef _OS_WINDOWS_
14+
# define DLLEXPORT __declspec(dllexport)
15+
#else
16+
# if defined(_OS_LINUX_) && !defined(_COMPILER_CLANG_)
17+
// Clang and ld disagree about the proper relocation for STV_PROTECTED, causing
18+
// linker errors.
19+
# define DLLEXPORT __attribute__ ((visibility("protected")))
20+
# else
21+
# define DLLEXPORT __attribute__ ((visibility("default")))
22+
# endif
23+
#endif
24+
25+
#ifdef _P64
26+
#define jint int64_t
27+
#else
28+
#define jint int32_t
29+
#endif
30+
31+
typedef struct {
32+
jint real;
33+
jint imag;
34+
} complex_t;
35+
36+
// We expect this to come from `libccalltest`
37+
extern complex_t ctest(complex_t);
38+
39+
DLLEXPORT complex_t dep_ctest(complex_t a) {
40+
return ctest(a);
41+
}

src/init.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ JL_DLLEXPORT void jl_postoutput_hook(void)
382382
}
383383

384384
void post_boot_hooks(void);
385+
void post_image_load_hooks(void);
385386

386387
JL_DLLEXPORT void *jl_libjulia_internal_handle;
387388
JL_DLLEXPORT void *jl_libjulia_handle;
@@ -888,6 +889,8 @@ static NOINLINE void _finish_julia_init(JL_IMAGE_SEARCH rel, jl_ptls_t ptls, jl_
888889
jl_module_run_initializer((jl_module_t*)mod);
889890
}
890891
JL_GC_POP();
892+
893+
post_image_load_hooks();
891894
}
892895

893896
if (jl_options.handle_signals == JL_OPTIONS_HANDLE_SIGNALS_ON)

src/jl_exported_data.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
XX(jl_voidpointer_type) \
127127
XX(jl_void_type) \
128128
XX(jl_weakref_type) \
129+
XX(jl_libdl_module) \
130+
XX(jl_libdl_dlopen_func) \
131+
XX(jl_lazy_library_type) \
129132

130133
// Data symbols that are defined inside the public libjulia
131134
#define JL_EXPORTED_DATA_SYMBOLS(XX) \

src/jltypes.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3439,6 +3439,24 @@ void post_boot_hooks(void)
34393439
}
34403440
export_small_typeof();
34413441
}
3442+
3443+
void post_image_load_hooks(void) {
3444+
// Ensure that `Base` has been loaded.
3445+
assert(jl_base_module != NULL);
3446+
3447+
jl_libdl_module = (jl_module_t *)jl_get_global(
3448+
jl_get_global(((jl_module_t *)jl_base_module), jl_symbol("Libc")),
3449+
jl_symbol("Libdl")
3450+
);
3451+
jl_libdl_dlopen_func = jl_get_global(
3452+
jl_libdl_module,
3453+
jl_symbol("dlopen")
3454+
);
3455+
jl_lazy_library_type = (jl_datatype_t *)jl_get_global(
3456+
jl_libdl_module,
3457+
jl_symbol("LazyLibrary")
3458+
);
3459+
}
34423460
#undef XX
34433461

34443462
#ifdef __cplusplus

src/julia.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,9 @@ extern JL_DLLIMPORT jl_value_t *jl_false JL_GLOBALLY_ROOTED;
883883
extern JL_DLLIMPORT jl_value_t *jl_nothing JL_GLOBALLY_ROOTED;
884884
extern JL_DLLIMPORT jl_value_t *jl_kwcall_func JL_GLOBALLY_ROOTED;
885885

886+
extern JL_DLLIMPORT jl_value_t *jl_libdl_dlopen_func JL_GLOBALLY_ROOTED;
887+
extern JL_DLLIMPORT jl_datatype_t *jl_lazy_library_type JL_GLOBALLY_ROOTED;
888+
886889
// gc -------------------------------------------------------------------------
887890

888891
struct _jl_gcframe_t {
@@ -1700,6 +1703,7 @@ extern JL_DLLIMPORT jl_module_t *jl_main_module JL_GLOBALLY_ROOTED;
17001703
extern JL_DLLIMPORT jl_module_t *jl_core_module JL_GLOBALLY_ROOTED;
17011704
extern JL_DLLIMPORT jl_module_t *jl_base_module JL_GLOBALLY_ROOTED;
17021705
extern JL_DLLIMPORT jl_module_t *jl_top_module JL_GLOBALLY_ROOTED;
1706+
extern JL_DLLIMPORT jl_module_t *jl_libdl_module JL_GLOBALLY_ROOTED;
17031707
JL_DLLEXPORT jl_module_t *jl_new_module(jl_sym_t *name, jl_module_t *parent);
17041708
JL_DLLEXPORT void jl_set_module_nospecialize(jl_module_t *self, int on);
17051709
JL_DLLEXPORT void jl_set_module_optlevel(jl_module_t *self, int lvl);

src/runtime_ccall.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,19 @@ void *jl_load_and_lookup(const char *f_lib, const char *f_name, _Atomic(void*) *
6666
extern "C" JL_DLLEXPORT
6767
void *jl_lazy_load_and_lookup(jl_value_t *lib_val, const char *f_name)
6868
{
69-
char *f_lib;
69+
void *lib_ptr;
7070

7171
if (jl_is_symbol(lib_val))
72-
f_lib = jl_symbol_name((jl_sym_t*)lib_val);
72+
lib_ptr = jl_get_library(jl_symbol_name((jl_sym_t*)lib_val));
7373
else if (jl_is_string(lib_val))
74-
f_lib = jl_string_data(lib_val);
75-
else
74+
lib_ptr = jl_get_library(jl_string_data(lib_val));
75+
else if (jl_lazy_library_type != NULL && jl_libdl_dlopen_func != NULL && jl_isa(lib_val, (jl_value_t *)jl_lazy_library_type)) {
76+
// Call `dlopen(lib_val)` if `lib_val` is a `LazyLibrary` object, use the returned handle as `lib_ptr`.
77+
lib_ptr = *((void **)jl_apply_generic(jl_libdl_dlopen_func, &lib_val, 1));
78+
} else
7679
jl_type_error("ccall", (jl_value_t*)jl_symbol_type, lib_val);
7780
void *ptr;
78-
jl_dlsym(jl_get_library(f_lib), f_name, &ptr, 1);
81+
jl_dlsym(lib_ptr, f_name, &ptr, 1);
7982
return ptr;
8083
}
8184

0 commit comments

Comments
 (0)