Standard library · System and tools

os.dl

Loading shared libraries at run time, looking up their functions and calling a function pointer with up to eight integer or pointer arguments.

import os.dl

Most programs link their libraries when they are built. This package is for the other case: the library is chosen while the program runs. Open loads a shared library (dlopen), Sym finds a function or a variable in it by name (dlsym), and Call calls the function pointer you get, with up to eight integer or pointer arguments, and returns the integer or pointer it returns. OpenSelf opens the running program itself, which gives you the C library and everything else already loaded. It is the basis for plug-ins, for probing optional libraries and for tools that load a function they have just written into executable memory.

Arguments and the result are size, an integer as wide as a pointer (64 bits on a 64 bit system). Pass a pointer by converting it, (p as void*) as size, and turn a returned pointer back with r as void*. A small integer is passed as it is. The package works on Linux, macOS and the BSDs; on Windows Supported is false and the functions return empty results.

What people get wrong:

  • Calling something that is not a function, or a function with the wrong number of arguments. Call cannot check that; a wrong pointer or a wrong count crashes the program or returns garbage. Give exactly the arguments the C function takes.
  • Calling a function that takes a struct by value or a variable argument list such as printf through Call or CallMixed. Those use a different calling convention. Floating point arguments need CallMixed, and a floating point result needs CallMixedF64.
  • Not checking for null. Open and Sym return null when the library or the symbol is missing, and Call on null returns 0; ask Error why.
  • Using a symbol after Close. Once the last user of a library closes it, its code is gone and a stale pointer crashes.
  • More than 8 arguments: Call refuses and returns 0.

Choosing a function by name at run time

The name of the function does not have to be known when the program is written. The example opens the running program itself with OpenSelf, looks each name up with Sym and calls what it finds. A name that does not exist gives null, which the example reports instead of calling it. Pass a library path to Open to do the same with a plug-in file.

os-dl-guide-plugin.salam
import os.dl
import str

func call_by_name(lib: void*, name: str, arg: size): str:
    fn := dl.Sym(lib, name)
    if fn == null:
        ret name + ": not found"
    end
    ret name + ": " + str.FromInt(dl.Call(fn, [arg]) as i64)
end

func main:
    if not dl.Supported():
        println "dynamic loading is not available here"
        ret
    end
    lib := dl.OpenSelf()
    if lib == null:
        println "cannot open the program:", dl.Error()
        ret
    end
    defer _closed := dl.Close(lib)
    text := "salam"
    println call_by_name(lib, "strlen", (text as void*) as size)
    println call_by_name(lib, "labs", (0 - 42) as size)
    println call_by_name(lib, "no_such_function", 1 as size)
end
Outputstrlen: 5 labs: 42 no_such_function: not found

Opening a library by path

Library file names differ between systems, so a portable program tries a few and uses the first that opens. Open returns null for one that does not exist and Error says why. Always pair a successful Open with Close, and do not use a symbol after its library is closed.

os-dl-guide-open.salam
import os.dl
import str

func open_first(paths: str[]): void*:
    each path in paths:
        lib := dl.Open(path)
        if lib != null:
            ret lib
        end
    end
    ret null
end

func main:
    if not dl.Supported():
        println "dynamic loading is not available here"
        ret
    end
    lib := open_first(["libno-such-library.so", "libc.so.6", "libc.dylib", "/usr/lib/libSystem.B.dylib", "libc.so.7"])
    if lib == null:
        println "no C library found:", dl.Error()
        ret
    end
    defer _closed := dl.Close(lib)
    println "opened a C library"
    text := "shared library"
    length := dl.Call(dl.Sym(lib, "strlen"), [(text as void*) as size])
    println "strlen says", str.FromInt(length as i64)
end
Outputopened a C library strlen says 14

Functions

Supported

func Supported(): bool
True where dynamic loading works (every system except Windows).
os-dl-Call.salam
import os.dl

func main:
    if not dl.Supported():
        println "not supported here"
        ret
    end
    lib := dl.OpenSelf()
    defer dl.Close(lib)
    text := "hello, world"
    length := dl.Call(dl.Sym(lib, "strlen"), [(text as void*) as size])
    println "strlen:", length
    println "labs:", dl.Call(dl.Sym(lib, "labs"), [(0 - 42) as size])
    println "missing symbol:", dl.Sym(lib, "no_such_function") == null
    missing := dl.Open("libno-such-library.so.9")
    println "missing library:", missing == null, dl.Error().len() > 0
end
Outputstrlen: 12 labs: 42 missing symbol: true missing library: true true

Open

func Open(path: str): void*
Loads the shared library at path (a name such as libm.so.6 is searched for by the system) and returns its handle, or null.

The example for this item is shown with the ones above: os-dl-Call.salam

OpenSelf

func OpenSelf(): void*
Opens the running program, so Sym can find the C library and anything else already loaded. null where it is not supported.

The example for this item is shown with the ones above: os-dl-Call.salam

Sym

func Sym(lib: void*, name: str): void*
The address of the function or variable called name in the library, or null. Use null checks, not exceptions.

The example for this item is shown with the ones above: os-dl-Call.salam

Close

func Close(lib: void*): bool
Gives the library back; true on success. Do not use its symbols afterwards.

The example for this item is shown with the ones above: os-dl-Call.salam

Error

func Error(): str
The system's text for the last failure of Open or Sym, or an empty string.

The example for this item is shown with the ones above: os-dl-Call.salam

Call

func Call(fn: void*, args: size): size
Calls the function at fn with the integer and pointer arguments in args (0 to 8 of them) and returns its integer or pointer result. A null pointer or more than 8 arguments gives 0. The arguments are passed in exactly the registers and stack slots a C compiler would use.

The example for this item is shown with the ones above: os-dl-Call.salam

CallMixed

func CallMixed(fn: void*, ints: size, dbls: f64): size
Like Call for a function that takes integer or pointer arguments (at most 6 in ints) and floating point arguments (at most 8 in dbls) and returns an integer or pointer. On x86-64 (System V) and 64 bit ARM the two kinds go in separate registers, so the order in which they are mixed in the C prototype does not matter: ldexp(double, int) is ints = [exp] and dbls = [x]. Not for variable argument lists. A null pointer or too many arguments gives 0.
os-dl-CallMixed.salam
import os.dl

func main:
    if not dl.Supported():
        println "not supported here"
        ret
    end
    lib := dl.OpenSelf()
    defer dl.Close(lib)
    mut m := lib
    if SALAM_OS_LINUX:
        m = dl.Open("libm.so.6")
    end
    println "pow:", dl.CallMixedF64(dl.Sym(m, "pow"), [0 as size], [2.0, 10.0])
    println "ldexp:", dl.CallMixedF64(dl.Sym(m, "ldexp"), [3 as size], [1.5])
    println "lround:", dl.CallMixed(dl.Sym(m, "lround"), [0 as size], [2.6])
end
Outputpow: 1024 ldexp: 12 lround: 3

CallMixedF64

func CallMixedF64(fn: void*, ints: size, dbls: f64): f64
The same call for a function that returns a double, such as pow or hypot.

The example for this item is shown with the ones above: os-dl-CallMixed.salam