Skip to content

Commit ca5b4a3

Browse files
committed
Libdl: Add comprehensive documentation for LazyLibrary
Adds documentation for `Libdl.LazyLibrary` and related types. Also extends the C calling manual with an expanded section on using `LazyLibrary` for lazy library loading, including practical examples of platform-specific libraries, dependency management, lazy path construction, and initialization callbacks. Remove a few oddly confusing or incorrect notes as well. Also fix a `copy` oddly implemented as `convert(Vector{LazyLibrary}, convert(Vector{Any}, LazyLibrary[]))` from #59233 to just call `copy`. 🤖 Generated with help by Claude Code.
1 parent a92e12a commit ca5b4a3

File tree

5 files changed

+252
-55
lines changed

5 files changed

+252
-55
lines changed

Compiler/src/abstractinterpretation.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3484,6 +3484,10 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, sstate:
34843484
callee = e.args[1]
34853485
if isexpr(callee, :tuple)
34863486
if length(callee.args) >= 1
3487+
# Evaluate the arguments to constrain the world, effects, and other info for codegen,
3488+
# but note there is an implied `if !=(C_NULL)` branch here that might read data
3489+
# in a different world (the exact cache behavior is unspecified), so we do not use
3490+
# these results to refine reachability of the subsequent foreigncall.
34873491
abstract_eval_value(interp, callee.args[1], sstate, sv)
34883492
if length(callee.args) >= 2
34893493
abstract_eval_value(interp, callee.args[2], sstate, sv)

base/libdl.jl

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,22 @@ end
319319

320320

321321
"""
322-
LazyLibraryPath
322+
LazyLibraryPath(path_pieces...)
323323
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.
324+
Helper type for lazily constructed library paths for use with [`LazyLibrary`](@ref).
325+
Path pieces are stored unevaluated and joined with `joinpath()` when the library is first
326+
accessed. Arguments must be able to have `string()` called on them.
327327
328+
# Example
329+
330+
```julia
331+
const mylib = LazyLibrary(LazyLibraryPath(artifact_dir, "lib", "libmylib.so.1.2.3"))
328332
```
329-
libfoo = LazyLibrary(LazyLibraryPath(prefix, "lib/libfoo.so.1.2.3"))
330-
```
333+
334+
!!! compat "Julia 1.11"
335+
`LazyLibraryPath` was added in Julia 1.11.
336+
337+
See also [`LazyLibrary`](@ref), [`BundledLazyLibraryPath`](@ref).
331338
"""
332339
struct LazyLibraryPath
333340
pieces::Tuple{Vararg{Any}}
@@ -347,34 +354,76 @@ end
347354
Base.string(::PrivateShlibdirGetter) = private_shlibdir()
348355

349356
"""
350-
BundledLazyLibraryPath
357+
BundledLazyLibraryPath(subpath)
351358
352-
Helper type for lazily constructed library paths that are stored within the
353-
bundled Julia distribution, primarily for use by Base modules.
359+
Helper type for lazily constructed library paths within the Julia distribution.
360+
Constructs paths relative to Julia's private shared library directory.
354361
362+
Primarily used by Julia's standard library. For example:
363+
```julia
364+
const libgmp = LazyLibrary(BundledLazyLibraryPath("libgmp.so.10"))
355365
```
356-
libfoo = LazyLibrary(BundledLazyLibraryPath("libfoo.so.1.2.3"))
357-
```
366+
367+
!!! compat "Julia 1.11"
368+
`BundledLazyLibraryPath` was added in Julia 1.11.
369+
370+
See also [`LazyLibrary`](@ref), [`LazyLibraryPath`](@ref).
358371
"""
359372
BundledLazyLibraryPath(subpath) = LazyLibraryPath(PrivateShlibdirGetter(), subpath)
360373

361374
# Small helper struct to initialize a LazyLibrary with its initial set of dependencies
362375
struct InitialDependencies
363-
dependencies::Vector{Any}
376+
dependencies::Vector{LazyLibrary}
364377
end
365-
(init::InitialDependencies)() = convert(Vector{LazyLibrary}, init.dependencies)
378+
(init::InitialDependencies)() = copy(init.dependencies)
366379

367380
"""
368-
LazyLibrary(name, flags = <default dlopen flags>,
381+
LazyLibrary(name; flags = <default dlopen flags>,
369382
dependencies = LazyLibrary[], on_load_callback = nothing)
370383
371-
Represents a lazily-loaded library that opens itself and its dependencies on first usage
372-
in a `dlopen()`, `dlsym()`, or `ccall()` usage. While this structure contains the
373-
ability to run arbitrary code on first load via `on_load_callback`, we caution that this
374-
should be used sparingly, as it is not expected that `ccall()` should result in large
375-
amounts of Julia code being run. You may call `ccall()` from within the
376-
`on_load_callback` but only for the current library and its dependencies, and user should
377-
not call `wait()` on any tasks within the on load callback.
384+
Represents a lazily-loaded shared library that delays loading itself and its dependencies
385+
until first use in a `ccall()`, `@ccall`, `dlopen()`, `dlsym()`, `dlpath()`, or `cglobal()`.
386+
This is a thread-safe mechanism for on-demand library initialization.
387+
388+
# Arguments
389+
390+
- `name`: Library name (or lazy path computation) as a `String`,
391+
[`LazyLibraryPath`](@ref), or [`BundledLazyLibraryPath`](@ref).
392+
- `flags`: Optional `dlopen` flags (default: `RTLD_LAZY | RTLD_DEEPBIND`). See [`dlopen`](@ref).
393+
- `dependencies`: Vector of `LazyLibrary` object references to load before this one.
394+
- `on_load_callback`: Optional function to run arbitrary code on first load (use sparingly,
395+
as it is not expected that `ccall()` should result in large amounts of Julia code being run.
396+
You may call `ccall()` from within the `on_load_callback` but only for the current library
397+
and its dependencies, and user should not call `wait()` on any tasks within the on load
398+
callback as they may deadlock).
399+
400+
The dlopen operation is thread-safe: only one thread loads the library, acquired after the
401+
release store of the reference to each dependency from loading of each dependency. Other
402+
tasks block until loading completes. The handle is then cached and reused for all subsequent
403+
calls (there is no dlclose for lazy library and dlclose should not be called on the returned
404+
handled).
405+
406+
# Examples
407+
408+
```julia
409+
# Basic usage
410+
const mylib = LazyLibrary("libmylib")
411+
@ccall mylib.myfunc(42::Cint)::Cint
412+
413+
# With dependencies
414+
const libfoo = LazyLibrary("libfoo")
415+
const libbar = LazyLibrary("libbar"; dependencies=[libfoo])
416+
```
417+
418+
For more examples including platform-specific libraries, lazy path construction, and
419+
migration from `__init__()` patterns, see the manual section on
420+
[Using LazyLibrary for Lazy Loading](@ref man-lazylibrary).
421+
422+
!!! compat "Julia 1.11"
423+
`LazyLibrary` was added in Julia 1.11.
424+
425+
See also [`LazyLibraryPath`](@ref), [`BundledLazyLibraryPath`](@ref), [`dlopen`](@ref),
426+
[`dlsym`](@ref), [`add_dependency!`](@ref).
378427
"""
379428
mutable struct LazyLibrary
380429
# Name and flags to open with
@@ -400,7 +449,7 @@ mutable struct LazyLibrary
400449
path,
401450
UInt32(flags),
402451
Base.OncePerProcess{Vector{LazyLibrary}}(
403-
InitialDependencies(collect(dependencies))
452+
InitialDependencies(dependencies)
404453
),
405454
on_load_callback,
406455
Base.ReentrantLock(),
@@ -411,6 +460,23 @@ end
411460

412461
# We support adding dependencies only because of very special situations
413462
# such as LBT needing to have OpenBLAS_jll added as a dependency dynamically.
463+
"""
464+
add_dependency!(library::LazyLibrary, dependency::LazyLibrary)
465+
466+
Dynamically add a dependency that must be loaded before `library`. Only needed when
467+
dependencies cannot be determined at construction time.
468+
469+
!!! warning
470+
Dependencies added with this function are **ephemeral** and only persist within the
471+
current process. They will not persist across precompilation boundaries.
472+
473+
Prefer specifying dependencies in the `LazyLibrary` constructor when possible.
474+
475+
!!! compat "Julia 1.11"
476+
`add_dependency!` was added in Julia 1.11.
477+
478+
See also [`LazyLibrary`](@ref).
479+
"""
414480
function add_dependency!(ll::LazyLibrary, dep::LazyLibrary)
415481
@lock ll.lock begin
416482
push!(ll.dependencies(), dep)

doc/src/manual/calling-c-and-fortran-code.md

Lines changed: 149 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -854,18 +854,135 @@ it must be handled in other ways.
854854

855855
In some cases, the exact name or path of the needed library is not known in
856856
advance and must be computed at run time. To handle such cases, the library
857-
component specification can be a value such as `Libdl.LazyLibrary`. For
858-
example, in `@ccall blas.dgemm()`, there can be a global defined as `const blas
859-
= LazyLibrary("libblas")`. The runtime will call `dlsym(:dgemm, dlopen(blas))`
860-
when the `@ccall` itself is executed. The `Libdl.dlopen` function can be
861-
overloaded for custom types to provide alternate behaviors. However, it is
862-
assumed that the library location does not change once it is determined, so the
863-
result of the call can be cached and reused. Therefore, the number of times the
864-
expression executes is unspecified, and returning different values for multiple
865-
calls results in unspecified behavior.
866-
867-
If even more flexibility is needed, it is possible
868-
to use computed values as function names by staging through [`eval`](@ref) as follows:
857+
component specification can be a value such as `Libdl.LazyLibrary`. The runtime
858+
will call `Libdl.dlopen` on that object when first used by a `ccall`.
859+
860+
### [Using LazyLibrary for Lazy Loading](@id man-lazylibrary)
861+
862+
[`Libdl.LazyLibrary`](@ref) provides a thread-safe mechanism for deferring library loading
863+
until first use. This is the recommended approach for library initialization in modern Julia code.
864+
865+
A `LazyLibrary` represents a library that opens itself (and its dependencies) automatically
866+
on first use in a `ccall()`, `@ccall`, `dlopen()`, `dlsym()`, `dlpath()`, or `cglobal()`.
867+
The library is loaded exactly once in a thread-safe manner, and subsequent calls reuse the
868+
loaded library handle.
869+
870+
#### Basic Usage
871+
872+
```julia
873+
using Libdl
874+
875+
# Define a LazyLibrary as a const for optimal performance
876+
const libz = LazyLibrary("libz")
877+
878+
# Use directly in @ccall - library loads automatically on first call
879+
@ccall libz.deflate(strm::Ptr{Cvoid}, flush::Cint)::Cint
880+
881+
# Also works with ccall
882+
ccall((:inflate, libz), Cint, (Ptr{Cvoid}, Cint), strm, flush)
883+
```
884+
885+
#### Platform-Specific Libraries
886+
887+
For code that needs to work across different platforms:
888+
889+
```julia
890+
const mylib = LazyLibrary(
891+
if Sys.iswindows()
892+
"mylib.dll"
893+
elseif Sys.isapple()
894+
"libmylib.dylib"
895+
else
896+
"libmylib.so"
897+
end
898+
)
899+
```
900+
901+
#### Libraries with Dependencies
902+
903+
When a library depends on other libraries, specify the dependencies to ensure
904+
they load in the correct order:
905+
906+
```julia
907+
const libfoo = LazyLibrary("libfoo")
908+
const libbar = LazyLibrary("libbar"; dependencies=[libfoo])
909+
910+
# When libbar is first used, libfoo is loaded first automatically
911+
@ccall libbar.bar_function(x::Cint)::Cint
912+
```
913+
914+
#### Lazy Path Construction
915+
916+
For libraries whose paths are determined at runtime, use `LazyLibraryPath`:
917+
918+
```julia
919+
# Path is constructed when library is first accessed
920+
const mylib = LazyLibrary(LazyLibraryPath(artifact_dir, "lib", "libmylib.so"))
921+
```
922+
923+
#### Initialization Callbacks
924+
925+
If a library requires initialization after loading:
926+
927+
```julia
928+
const mylib = LazyLibrary("libmylib";
929+
on_load_callback = () -> @ccall mylib.initialize()::Cvoid
930+
)
931+
```
932+
933+
!!! warning
934+
The `on_load_callback` should be minimal and must not call `wait()` on any tasks.
935+
It is called exactly once by the thread that loads the library.
936+
937+
#### Conversion from `__init__()` Pattern
938+
939+
Before `LazyLibrary`, library paths were often computed in `__init__()` functions.
940+
This pattern can be replaced with `LazyLibrary` for better performance and thread safety.
941+
942+
Old pattern using `__init__()`:
943+
944+
```julia
945+
# Old: Library path computed in __init__()
946+
libmylib_path = ""
947+
948+
function __init__(
949+
# Loads library on startup, whether it is used or not
950+
global libmylib_path = find_library(["libmylib"])
951+
end
952+
953+
function myfunc(x)
954+
ccall((:cfunc, libmylib_path), Cint, (Cint,), x)
955+
end
956+
```
957+
958+
New pattern using `LazyLibrary`:
959+
960+
```julia
961+
# New: Library as const, no __init__() needed
962+
const libmylib = LazyLibrary("libmylib")
963+
964+
function myfunc(x)
965+
# Library loads automatically just before calling `cfunc`
966+
@ccall libmylib.cfunc(x::Cint)::Cint
967+
end
968+
```
969+
970+
For more details, see the [`Libdl.LazyLibrary`](@ref) documentation.
971+
972+
### Overloading `dlopen` for Custom Types
973+
974+
The runtime will call `dlsym(:function, dlopen(library)::Ptr{Cvoid})` when a `@ccall` is executed.
975+
The `Libdl.dlopen` function can be overloaded for custom types to provide alternate behaviors.
976+
However, it is assumed that the library location and handle does not change
977+
once it is determined, so the result of the call may be cached and reused.
978+
Therefore, the number of times the `dlopen` expression executes is unspecified,
979+
and returning different values for multiple calls will results in unspecified
980+
(but valid) behavior.
981+
982+
### Computed Function Names
983+
984+
If even more flexibility is needed, it is possible to use computed values as
985+
function names by staging through [`eval`](@ref) as follows:
869986
870987
```julia
871988
@eval @ccall "lib".$(string("a", "b"))()::Cint
@@ -876,38 +993,37 @@ expression, which is then evaluated. Keep in mind that `eval` only operates at t
876993
so within this expression local variables will not be available (unless their values are substituted
877994
with `$`). For this reason, `eval` is typically only used to form top-level definitions, for example
878995
when wrapping libraries that contain many similar functions.
879-
A similar example can be constructed for [`@cfunction`](@ref).
880-
881-
However, doing this will also be very slow and leak memory, so you should usually avoid this and instead keep
882-
reading.
883-
The next section discusses how to use indirect calls to efficiently achieve a similar effect.
884996
885-
## Indirect Calls
997+
### Indirect Calls
886998
887-
The first argument to `@ccall` can also be an expression evaluated at run time. In this
888-
case, the expression must evaluate to a `Ptr`, which will be used as the address of the native
889-
function to call. This behavior occurs when the first `@ccall` argument contains references
890-
to non-constants, such as local variables, function arguments, or non-constant globals.
999+
The first argument to `@ccall` can also be an expression to be evaluated at run
1000+
time, each time it is used. In this case, the expression must evaluate to a
1001+
`Ptr`, which will be used as the address of the native function to call. This
1002+
behavior occurs when the first `@ccall` argument is marked with `$` and when
1003+
the first `ccall` argument is not a simple constant literal or expression in
1004+
`()`. The argument can be any expression and can use local variables and
1005+
arguments and can return a different value every time.
8911006
892-
For example, you might look up the function via `dlsym`,
893-
then cache it in a shared reference for that session. For example:
1007+
For example, you might implement a macro similar to `cglobal` that looks up the
1008+
function via `dlsym`, then caches the pointer in a shared reference (which is
1009+
auto reset to C_NULL during precompile saving).
1010+
For example:
8941011
8951012
```julia
8961013
macro dlsym(lib, func)
897-
z = Ref{Ptr{Cvoid}}(C_NULL)
1014+
z = Ref(C_NULL)
8981015
quote
899-
let zlocal = $z[]
900-
if zlocal == C_NULL
901-
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
902-
$z[] = zlocal
903-
end
904-
zlocal
1016+
local zlocal = $z[]
1017+
if zlocal == C_NULL
1018+
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
1019+
$z[] = zlocal
9051020
end
1021+
zlocal
9061022
end
9071023
end
9081024

909-
mylibvar = Libdl.dlopen("mylib")
910-
@ccall $(@dlsym(mylibvar, "myfunc"))()::Cvoid
1025+
const mylibvar = LazyLibrary("mylib")
1026+
@ccall $(@dlsym(dlopen(mylibvar), "myfunc"))()::Cvoid
9111027
```
9121028
9131029
## Closure cfunctions

stdlib/Libdl/docs/src/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,12 @@ Libdl.dlpath
2121
Libdl.find_library
2222
Libdl.DL_LOAD_PATH
2323
```
24+
25+
# Lazy Library Loading
26+
27+
```@docs
28+
Libdl.LazyLibrary
29+
Libdl.LazyLibraryPath
30+
Libdl.BundledLazyLibraryPath
31+
Libdl.add_dependency!
32+
```

stdlib/Libdl/src/Libdl.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export DL_LOAD_PATH, RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOC
1111
RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW, dlclose, dlopen, dlopen_e, dlsym, dlsym_e,
1212
dlpath, find_library, dlext, dllist, LazyLibrary, LazyLibraryPath, BundledLazyLibraryPath
1313

14+
public add_dependency!
15+
1416
import Base.Libc.Libdl: DL_LOAD_PATH, RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL,
1517
RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW, dlclose, dlopen, dlopen_e, dlsym, dlsym_e,
1618
dlpath, find_library, dlext, dllist, LazyLibrary, LazyLibraryPath,

0 commit comments

Comments
 (0)