diff --git a/Compiler/src/abstractinterpretation.jl b/Compiler/src/abstractinterpretation.jl index f755d8f02dab2..6b409f057eadf 100644 --- a/Compiler/src/abstractinterpretation.jl +++ b/Compiler/src/abstractinterpretation.jl @@ -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) diff --git a/base/libdl.jl b/base/libdl.jl index cc7ff35e9ec09..d10434bb8c610 100644 --- a/base/libdl.jl +++ b/base/libdl.jl @@ -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}} @@ -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 = , + LazyLibrary(name; 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 +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 @@ -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 @@ -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(), @@ -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) diff --git a/doc/src/manual/calling-c-and-fortran-code.md b/doc/src/manual/calling-c-and-fortran-code.md index bf0429fdff3f0..2e6098864cabd 100644 --- a/doc/src/manual/calling-c-and-fortran-code.md +++ b/doc/src/manual/calling-c-and-fortran-code.md @@ -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`: + +```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 + +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 @@ -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 diff --git a/stdlib/Libdl/docs/src/index.md b/stdlib/Libdl/docs/src/index.md index 2d7ef2fffc41a..3cd69486e5155 100644 --- a/stdlib/Libdl/docs/src/index.md +++ b/stdlib/Libdl/docs/src/index.md @@ -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! +``` diff --git a/stdlib/Libdl/src/Libdl.jl b/stdlib/Libdl/src/Libdl.jl index 2a8f800c69194..8556387786571 100644 --- a/stdlib/Libdl/src/Libdl.jl +++ b/stdlib/Libdl/src/Libdl.jl @@ -11,6 +11,8 @@ export DL_LOAD_PATH, RTLD_DEEPBIND, RTLD_FIRST, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOC RTLD_NODELETE, RTLD_NOLOAD, RTLD_NOW, dlclose, dlopen, dlopen_e, dlsym, dlsym_e, dlpath, find_library, dlext, dllist, LazyLibrary, LazyLibraryPath, BundledLazyLibraryPath +public add_dependency! + import Base.Libc.Libdl: 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, LazyLibrary, LazyLibraryPath,