Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions Compiler/src/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,10 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, sstate:
callee = e.args[1]
if isexpr(callee, :tuple)
if length(callee.args) >= 1
# Evaluate the arguments to constrain the world, effects, and other info for codegen,
# but note there is an implied `if !=(C_NULL)` branch here that might read data
# in a different world (the exact cache behavior is unspecified), so we do not use
# these results to refine reachability of the subsequent foreigncall.
abstract_eval_value(interp, callee.args[1], sstate, sv)
if length(callee.args) >= 2
abstract_eval_value(interp, callee.args[2], sstate, sv)
Expand Down
114 changes: 90 additions & 24 deletions base/libdl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,22 @@ end


"""
LazyLibraryPath
LazyLibraryPath(path_pieces...)

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.
Helper type for lazily constructed library paths for use with [`LazyLibrary`](@ref).
Path pieces are stored unevaluated and joined with `joinpath()` when the library is first
accessed. Arguments must be able to have `string()` called on them.

# Example

```julia
const mylib = LazyLibrary(LazyLibraryPath(artifact_dir, "lib", "libmylib.so.1.2.3"))
```
libfoo = LazyLibrary(LazyLibraryPath(prefix, "lib/libfoo.so.1.2.3"))
```

!!! compat "Julia 1.11"
`LazyLibraryPath` was added in Julia 1.11.

See also [`LazyLibrary`](@ref), [`BundledLazyLibraryPath`](@ref).
"""
struct LazyLibraryPath
pieces::Tuple{Vararg{Any}}
Expand All @@ -347,34 +354,76 @@ end
Base.string(::PrivateShlibdirGetter) = private_shlibdir()

"""
BundledLazyLibraryPath
BundledLazyLibraryPath(subpath)

Helper type for lazily constructed library paths that are stored within the
bundled Julia distribution, primarily for use by Base modules.
Helper type for lazily constructed library paths within the Julia distribution.
Constructs paths relative to Julia's private shared library directory.

Primarily used by Julia's standard library. For example:
```julia
const libgmp = LazyLibrary(BundledLazyLibraryPath("libgmp.so.10"))
```
libfoo = LazyLibrary(BundledLazyLibraryPath("libfoo.so.1.2.3"))
```

!!! compat "Julia 1.11"
`BundledLazyLibraryPath` was added in Julia 1.11.

See also [`LazyLibrary`](@ref), [`LazyLibraryPath`](@ref).
"""
BundledLazyLibraryPath(subpath) = LazyLibraryPath(PrivateShlibdirGetter(), subpath)

# Small helper struct to initialize a LazyLibrary with its initial set of dependencies
struct InitialDependencies
dependencies::Vector{Any}
struct InitialDependencies{T}
dependencies::Vector{T}
end
(init::InitialDependencies)() = convert(Vector{LazyLibrary}, init.dependencies)
(init::InitialDependencies)() = copy(init.dependencies)

"""
LazyLibrary(name, flags = <default dlopen flags>,
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.
Represents a lazily-loaded shared library that delays loading itself and its dependencies
until first use in a `ccall()`, `@ccall`, `dlopen()`, `dlsym()`, `dlpath()`, or `cglobal()`.
This is a thread-safe mechanism for on-demand library initialization.

# Arguments

- `name`: Library name (or lazy path computation) as a `String`,
[`LazyLibraryPath`](@ref), or [`BundledLazyLibraryPath`](@ref).
- `flags`: Optional `dlopen` flags (default: `RTLD_LAZY | RTLD_DEEPBIND`). See [`dlopen`](@ref).
- `dependencies`: Vector of `LazyLibrary` object references to load before this one.
- `on_load_callback`: Optional function to run arbitrary code on first load (use 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 as they may deadlock).

The dlopen operation is thread-safe: only one thread loads the library, acquired after the
release store of the reference to each dependency from loading of each dependency. Other
tasks block until loading completes. The handle is then cached and reused for all subsequent
Copy link
Member

Choose a reason for hiding this comment

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

When you say other tasks, you mean other dlopen() tasks, right? Not all tasks?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I guess we should clarify that it is only of this specific library (e.g. so that doing dlopen in parallel is not UB for deadlocks)

calls (there is no dlclose for lazy library and dlclose should not be called on the returned
handled).

# Examples

```julia
# Basic usage
const mylib = LazyLibrary("libmylib")
@ccall mylib.myfunc(42::Cint)::Cint

# With dependencies
const libfoo = LazyLibrary("libfoo")
const libbar = LazyLibrary("libbar"; dependencies=[libfoo])
```

For more examples including platform-specific libraries, lazy path construction, and
migration from `__init__()` patterns, see the manual section on
[Using LazyLibrary for Lazy Loading](@ref man-lazylibrary).

!!! compat "Julia 1.11"
`LazyLibrary` was added in Julia 1.11.

See also [`LazyLibraryPath`](@ref), [`BundledLazyLibraryPath`](@ref), [`dlopen`](@ref),
[`dlsym`](@ref), [`add_dependency!`](@ref).
"""
mutable struct LazyLibrary
# Name and flags to open with
Expand All @@ -386,7 +435,7 @@ mutable struct LazyLibrary
# The OncePerProcess is introduced here so that any registered dependencies are
# always ephemeral to a given process (instead of, e.g., persisting depending
# on whether they were added in the process where this LazyLibrary was created)
dependencies::Base.OncePerProcess{Vector{LazyLibrary}, InitialDependencies}
dependencies::Base.OncePerProcess{Vector{LazyLibrary}, InitialDependencies{LazyLibrary}}

# Function that get called once upon initial load
on_load_callback
Expand All @@ -400,7 +449,7 @@ mutable struct LazyLibrary
path,
UInt32(flags),
Base.OncePerProcess{Vector{LazyLibrary}}(
InitialDependencies(collect(dependencies))
InitialDependencies{LazyLibrary}(dependencies)
),
on_load_callback,
Base.ReentrantLock(),
Expand All @@ -411,6 +460,23 @@ end

# We support adding dependencies only because of very special situations
# such as LBT needing to have OpenBLAS_jll added as a dependency dynamically.
"""
add_dependency!(library::LazyLibrary, dependency::LazyLibrary)

Dynamically add a dependency that must be loaded before `library`. Only needed when
dependencies cannot be determined at construction time.

!!! warning
Dependencies added with this function are **ephemeral** and only persist within the
current process. They will not persist across precompilation boundaries.

Prefer specifying dependencies in the `LazyLibrary` constructor when possible.

!!! compat "Julia 1.11"
`add_dependency!` was added in Julia 1.11.

See also [`LazyLibrary`](@ref).
"""
function add_dependency!(ll::LazyLibrary, dep::LazyLibrary)
@lock ll.lock begin
push!(ll.dependencies(), dep)
Expand Down
182 changes: 149 additions & 33 deletions doc/src/manual/calling-c-and-fortran-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,18 +854,135 @@ it must be handled in other ways.

In some cases, the exact name or path of the needed library is not known in
advance and must be computed at run time. To handle such cases, the library
component specification can be a value such as `Libdl.LazyLibrary`. For
example, in `@ccall blas.dgemm()`, there can be a global defined as `const blas
= LazyLibrary("libblas")`. The runtime will call `dlsym(:dgemm, dlopen(blas))`
when the `@ccall` itself is executed. The `Libdl.dlopen` function can be
overloaded for custom types to provide alternate behaviors. However, it is
assumed that the library location does not change once it is determined, so the
result of the call can be cached and reused. Therefore, the number of times the
expression executes is unspecified, and returning different values for multiple
calls results in unspecified behavior.

If even more flexibility is needed, it is possible
to use computed values as function names by staging through [`eval`](@ref) as follows:
component specification can be a value such as `Libdl.LazyLibrary`. The runtime
will call `Libdl.dlopen` on that object when first used by a `ccall`.

### [Using LazyLibrary for Lazy Loading](@id man-lazylibrary)

[`Libdl.LazyLibrary`](@ref) provides a thread-safe mechanism for deferring library loading
until first use. This is the recommended approach for library initialization in modern Julia code.

A `LazyLibrary` represents a library that opens itself (and its dependencies) automatically
on first use in a `ccall()`, `@ccall`, `dlopen()`, `dlsym()`, `dlpath()`, or `cglobal()`.
The library is loaded exactly once in a thread-safe manner, and subsequent calls reuse the
loaded library handle.

#### Basic Usage

```julia
using Libdl

# Define a LazyLibrary as a const for optimal performance
const libz = LazyLibrary("libz")

# Use directly in @ccall - library loads automatically on first call
@ccall libz.deflate(strm::Ptr{Cvoid}, flush::Cint)::Cint

# Also works with ccall
ccall((:inflate, libz), Cint, (Ptr{Cvoid}, Cint), strm, flush)
```

#### Platform-Specific Libraries

For code that needs to work across different platforms:

```julia
const mylib = LazyLibrary(
if Sys.iswindows()
"mylib.dll"
elseif Sys.isapple()
"libmylib.dylib"
else
"libmylib.so"
end
)
```

#### Libraries with Dependencies

When a library depends on other libraries, specify the dependencies to ensure
they load in the correct order:

```julia
const libfoo = LazyLibrary("libfoo")
const libbar = LazyLibrary("libbar"; dependencies=[libfoo])

# When libbar is first used, libfoo is loaded first automatically
@ccall libbar.bar_function(x::Cint)::Cint
```

#### Lazy Path Construction

For libraries whose paths are determined at runtime, use `LazyLibraryPath`:
Copy link
Member

Choose a reason for hiding this comment

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

Maybe make a small note as to why you didn't need to use LazyLibraryPath when doing the OS-specific paths above.


```julia
# Path is constructed when library is first accessed
const mylib = LazyLibrary(LazyLibraryPath(artifact_dir, "lib", "libmylib.so"))
```

#### Initialization Callbacks

If a library requires initialization after loading:

```julia
const mylib = LazyLibrary("libmylib";
on_load_callback = () -> @ccall mylib.initialize()::Cvoid
)
```

!!! warning
The `on_load_callback` should be minimal and must not call `wait()` on any tasks.
It is called exactly once by the thread that loads the library.

#### Conversion from `__init__()` Pattern

Before `LazyLibrary`, library paths were often computed in `__init__()` functions.
This pattern can be replaced with `LazyLibrary` for better performance and thread safety.

Old pattern using `__init__()`:

```julia
# Old: Library path computed in __init__()
libmylib_path = ""

function __init__(
# Loads library on startup, whether it is used or not
global libmylib_path = find_library(["libmylib"])
end

function myfunc(x)
ccall((:cfunc, libmylib_path), Cint, (Cint,), x)
end
```

New pattern using `LazyLibrary`:

```julia
# New: Library as const, no __init__() needed
const libmylib = LazyLibrary("libmylib")

function myfunc(x)
# Library loads automatically just before calling `cfunc`
@ccall libmylib.cfunc(x::Cint)::Cint
end
```

For more details, see the [`Libdl.LazyLibrary`](@ref) documentation.

### Overloading `dlopen` for Custom Types
Copy link
Member

Choose a reason for hiding this comment

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

What is this section for? Are you trying to warn other people who might overload dlopen() (like we did for LazyLibrary) or are you saying there's some reason someone might want to overload dlopen() for LazyLibrary itself?

Copy link
Member Author

Choose a reason for hiding this comment

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

You can write your own LazyLibrary-like types. This is documentation for those fools who write LazyLibrary types, like us.

Copy link
Member

Choose a reason for hiding this comment

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

Nice; maybe say that LazyLibrary is one such type, since I was a little confused at first, thinking this was further useful information under the LazyLibrary umbrella.


The runtime will call `dlsym(:function, dlopen(library)::Ptr{Cvoid})` when a `@ccall` is executed.
The `Libdl.dlopen` function can be overloaded for custom types to provide alternate behaviors.
However, it is assumed that the library location and handle does not change
once it is determined, so the result of the call may be cached and reused.
Therefore, the number of times the `dlopen` expression executes is unspecified,
and returning different values for multiple calls will results in unspecified
(but valid) behavior.

### Computed Function Names

If even more flexibility is needed, it is possible to use computed values as
function names by staging through [`eval`](@ref) as follows:

```julia
@eval @ccall "lib".$(string("a", "b"))()::Cint
Expand All @@ -876,38 +993,37 @@ expression, which is then evaluated. Keep in mind that `eval` only operates at t
so within this expression local variables will not be available (unless their values are substituted
with `$`). For this reason, `eval` is typically only used to form top-level definitions, for example
when wrapping libraries that contain many similar functions.
A similar example can be constructed for [`@cfunction`](@ref).

However, doing this will also be very slow and leak memory, so you should usually avoid this and instead keep
reading.
The next section discusses how to use indirect calls to efficiently achieve a similar effect.

## Indirect Calls
### Indirect Calls

The first argument to `@ccall` can also be an expression evaluated at run time. In this
case, the expression must evaluate to a `Ptr`, which will be used as the address of the native
function to call. This behavior occurs when the first `@ccall` argument contains references
to non-constants, such as local variables, function arguments, or non-constant globals.
The first argument to `@ccall` can also be an expression to be evaluated at run
time, each time it is used. In this case, the expression must evaluate to a
`Ptr`, which will be used as the address of the native function to call. This
behavior occurs when the first `@ccall` argument is marked with `$` and when
the first `ccall` argument is not a simple constant literal or expression in
`()`. The argument can be any expression and can use local variables and
arguments and can return a different value every time.

For example, you might look up the function via `dlsym`,
then cache it in a shared reference for that session. For example:
For example, you might implement a macro similar to `cglobal` that looks up the
function via `dlsym`, then caches the pointer in a shared reference (which is
auto reset to C_NULL during precompile saving).
For example:

```julia
macro dlsym(lib, func)
z = Ref{Ptr{Cvoid}}(C_NULL)
z = Ref(C_NULL)
quote
let zlocal = $z[]
if zlocal == C_NULL
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
$z[] = zlocal
end
zlocal
local zlocal = $z[]
if zlocal == C_NULL
zlocal = dlsym($(esc(lib))::Ptr{Cvoid}, $(esc(func)))::Ptr{Cvoid}
$z[] = zlocal
end
zlocal
end
end

mylibvar = Libdl.dlopen("mylib")
@ccall $(@dlsym(mylibvar, "myfunc"))()::Cvoid
const mylibvar = LazyLibrary("mylib")
@ccall $(@dlsym(dlopen(mylibvar), "myfunc"))()::Cvoid
```

## Closure cfunctions
Expand Down
9 changes: 9 additions & 0 deletions stdlib/Libdl/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ Libdl.dlpath
Libdl.find_library
Libdl.DL_LOAD_PATH
```

# Lazy Library Loading

```@docs
Libdl.LazyLibrary
Libdl.LazyLibraryPath
Libdl.BundledLazyLibraryPath
Libdl.add_dependency!
```
Loading