The Mojo ManualXYZ Homework Edition

⇩ Download ▾

10.5 Using Mojo's C foreign function interface to call C libraries

When you need functionality that's already available in a C library, you can call it directly from your Mojo code. Many libraries for graphics, databases, hardware control, signal processing, and scientific computing expose C APIs.

Mojo emits a direct native call, with no translation layer or extra runtime overhead. A C call from Mojo runs as fast as handwritten C.

C number types

C integer types don't have fixed sizes. Their sizes depend on the target platform and its C ABI. For example, int is commonly 32 bits, while long is 64 bits on Linux and macOS but 32 bits on Windows.

An ABI (application binary interface) defines how machine code passes arguments, returns values, and lays out data in memory.

Use the std.ffi module's type aliases when working with C APIs. They match the target platform's C ABI, so you don't need to worry about platform-specific size differences.

See the C type reference at the end of this page for a list of std.ffi type aliases and their equivalent Mojo types.

Call libc functions

libc is the C standard library. It provides functions for memory allocation, string manipulation, file I/O, and other common tasks. Mojo calls libc functions with external_call(). Mojo resolves the symbol for you, so you don't need to add anything to your build.

Import external_call from std.ffi. Parameterize it with the function name and return type. Then pass the function arguments in parentheses. Mojo infers the argument types from the values you pass, so there's nothing else to declare:

def external_call[
    callee: StaticString,
    return_type: RegisterPassable,
    *types: AnyType,
    num_fixed_args: OptionalReg[Int] = None,
](*args: *types) -> return_type

The following example calls the C abs() function, which returns the absolute value of an integer:

from std.ffi import external_call, c_int

def main():
    # int abs(int n);
    var n = external_call["abs", c_int](c_int(-42))
    print(t"Absolute value is 42: {n == 42}")  # True
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
Absolute value is 42: True

c_int is the std.ffi alias for C's int. It's 32 bits on every platform Mojo targets. Its Mojo counterpart is Int32.

Call variadic C functions

A variadic C function takes a variable number of arguments, like printf() and snprintf(). Pass num_fixed_args with the number of arguments declared before the ...:

from std.ffi import external_call, c_char, c_int, c_size_t

def main():
    # int snprintf(char *buf, size_t size, const char *fmt, ...);
    # Three fixed arguments, so num_fixed_args=3.
    var buf = Array[c_char, 64](uninitialized=True)
    var written = external_call["snprintf", c_int, num_fixed_args=3](
        buf.unsafe_ptr(),
        c_size_t(64),
        "score: %d/%d".as_c_string_span(),
        c_int(7),
        c_int(10),
    )
    print(t"wrote {written}: {String(unsafe_from_utf8_ptr=buf.unsafe_ptr())}")
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
wrote 11: score: 7/10

Without num_fixed_args, Mojo treats every argument as fixed. Some ABIs pass variadic arguments differently from fixed ones, so the call can work on one target and break on another.

Use shared libraries

An OwnedDLHandle owns a handle to a dynamically linked library with RAII semantics. Use it to load shared libraries and retrieve functions as Mojo callables, so you can work with libraries such as SQLite, libcurl, camera SDKs, GPU vendor libraries, and other native libraries.

Library names differ by platform, so use platform_map() to select the right one at compile time:

from std.ffi import OwnedDLHandle, c_double
from std.sys.info import platform_map

comptime LIBM = platform_map["libm", linux="libm.so.6", macos="libm.dylib"]()

def main() raises:
    var lib = OwnedDLHandle(LIBM)
    var sqrt = lib.get_function[c_double]("sqrt")
    print(sqrt(c_double(4.0)))  # Prints: 2.0
    # Library automatically closed when lib goes out of scope
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
2.0

If platform_map() has no value for the target, it raises a compilation error. It won't fall through to a library name for another platform.

Library names

Pass the library as any os.PathLike, such as a String or a Path. Mojo resolves the name at runtime. Use the bare name (libm.dylib) when the library is on the system search path, or a full path (path/to/libm.dylib) when it isn't.

On Linux, use the ABI-versioned runtime name, such as libm.so.6, instead of the unversioned libm.so. An ABI version doesn't necessarily match the library's release version. For example, libcurl 8.21 still uses libcurl.so.4.

The unversioned name belongs to the development package, where the static linker consumes it for options such as -lm. It's often a linker script rather than a library, so passing it to dlopen can fail with an invalid ELF header error. Find the shared libraries the dynamic linker knows about with:

ldconfig -p | grep libcurl

macOS uses one name for both purposes. libcurl.dylib is both what you link against and what you load.

If you omit the library name, OwnedDLHandle() opens the current process. This is another way to call libc functions and other symbols already linked into your program.

Availability checks

OwnedDLHandle loads libraries at runtime, so the library must be available when your program runs. If it can't be found, loading fails:

comptime LIBCURL = platform_map[
    "libcurl", linux="libcurl.so.4", macos="libcurl.dylib"
]()

try:
    var lib = OwnedDLHandle(LIBCURL)
    # use the optional feature
except:
    # fall back

You can guard against missing functions with check_symbol(). Use it to test for optional, versioned, or platform-specific features. The check works for both functions and exported globals:

comptime LIBM = platform_map[
    "libm", linux="libm.so.6", macos="libm.dylib"
]()

var lib = OwnedDLHandle(LIBM)
if lib.check_symbol("exp10"):
    var exp10 = lib.get_function[c_double]("exp10")
    print(exp10(c_double(2.0)))  # 100.0
else:
    print("exp10 not found in libm")

Retrieve functions by name

get_function() looks up a library function by name and returns a callable. Parameterize it with the C function's return type. Here's a curses example:

# WinPtr is a pointer to a curses window struct
var wgetch = lib.get_function[c_int]("wgetch")

# ... later

_ = wgetch(win)  # blocks until a key is pressed.

You don't declare the argument types. Mojo infers them from the values you pass at each call, and forwards them using the C calling convention.

Missing symbols raise errors.

Passing pointers

Many C APIs work with pointers. Mojo represents raw pointers with Pointer[T], where T is the pointed-to type. When a C API expects a void*, use .unsafe_bitcast[NoneType]() to produce an OpaquePointer.

For example:

var value: c_int = 42
var p = Pointer(to=value)  # Pointer to a C int
var opaque: OpaquePointer[origin_of(value)] = p.unsafe_bitcast[NoneType]()

Typed pointers

C functions often write results through a pointer you provide, rather than returning them. Pass Pointer(to=value) and C fills in the value. An imm function argument won't work, and, worse, it fails quietly, leaving the value unchanged. Use the mut convention or copy the value into a local var before your call.

This example passes a Mojo floating-point number to C's frexp, which splits it into a mantissa and an exponent:

from std.ffi import external_call, c_double, c_int

def main():
    # double frexp(double x, int *exp);
    # Returns the mantissa and writes the exponent through the pointer.
    var exponent: c_int = 0
    var mantissa = external_call["frexp", c_double](
        c_double(12.0), Pointer(to=exponent)
    )
    print(t"12.0 = {mantissa} * 2^{exponent}")  # 0.75 * 2^4
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
12.0 = 0.75 * 2^4

Opaque pointers

The C standard library provides qsort, a general-purpose sorting function.

qsort sorts its array in place. You provide a pointer to that array, its number of elements, the element size, and a comparison function. Whenever qsort compares two elements, it calls your Mojo-native comparison function.

The comparison function must be thin. That is, it can't capture any Mojo state as a closure. You must mark it with abi("C"), allowing qsort to call it across the FFI boundary.

The following example sorts a list of C integers. The compare() function receives two void* pointers, casts them back to c_int*, and returns the comparison result:

from std.ffi import external_call, c_int, c_size_t
from std.sys import size_of

def compare(
    a: OpaquePointer[mut=False, _],
    b: OpaquePointer[mut=False, _],
) abi("C") -> c_int:
    var a_value = a.unsafe_bitcast[c_int]()[]
    var b_value = b.unsafe_bitcast[c_int]()[]
    # `qsort` only needs to know which value is larger. Compare the values
    # instead of subtracting them. Large differences can overflow, producing
    # the wrong comparison result and sorting the values incorrectly.
    if a_value < b_value:
        return c_int(-1)
    return c_int(a_value > b_value)

def main() raises:
    var numbers: List[c_int] = [5, 2, 9, 1, 5, 6]
    var count = c_size_t(len(numbers))
    var size = c_size_t(size_of[c_int]())
    external_call["qsort", NoneType](
        numbers.unsafe_ptr(),
        count,
        size,
        compare,
    )
    print("Sorted numbers:", numbers)  # [1, 2, 5, 5, 6, 9]
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
Sorted numbers: [1, 2, 5, 5, 6, 9]

Passing structs

Struct pointers allow Mojo and C APIs to exchange structured data that goes beyond simple values. For example, clock_gettime() writes the system's monotonic time into a C struct timespec.

To read that data from Mojo, define a struct with a C-compatible layout and pass a pointer to it:

from std.ffi import external_call, c_int, c_long
from std.sys.info import platform_map

@fieldwise_init
struct CTimeSpec(RegisterPassable):  # Matches C's struct timespec.
    # CLOCK_MONOTONIC differs by platform
    comptime monotonic = c_int(
        platform_map["CLOCK_MONOTONIC", linux=1, macos=6]()
    )

    var tv_sec: c_long
    var tv_nsec: c_long

    @staticmethod
    def monotonic_nanos() raises -> c_long:
        var time_spec = Self(0, 0)
        if (
            external_call["clock_gettime", c_int](
                Self.monotonic,
                Pointer(to=time_spec),
            )
            != 0
        ):
            raise Error("clock_gettime failed")
        return time_spec.tv_sec * 1_000_000_000 + time_spec.tv_nsec

def main() raises:
    print(t"Monotonic time: {CTimeSpec.monotonic_nanos()} ns")
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
Monotonic time: 185040440367000 ns

C-compatible structs

C-compatible types are ordinary structs with two requirements:

Field order matters. Declare your fields in the same order as the C struct you're mirroring. Mojo uses the corresponding C layout, including padding required for field alignment:

# Mirrors C `div_t`: two ints, 8 bytes total.
@fieldwise_init
struct DivT(RegisterPassable):
    var quot: c_int
    var rem: c_int

def main() raises:
    var proc = OwnedDLHandle()  # No path: opens the current process

    var div = proc.get_function[DivT]("div")
    var d = div(c_int(7), c_int(3))
    print(t"div(7, 3): quot {d.quot} rem {d.rem}")  # 2 1

Passing lists, arrays, and spans

A Mojo List[T] stores its elements contiguously in memory, just like C arrays. You pass a list to C as a pointer plus a length, as shown in the qsort example.

Mojo list pointers are fragile. Operations that grow the list, such as append(), may move its storage and leave an earlier pointer stale. So get the pointer fresh, right before you use it, after any change to the list.

Span[T] is Mojo's built-in pointer-plus-length pair. It wraps a pointer to contiguous memory and stores a length. This gives you built-in bounds checking and safe iteration.

Array[T, length] is Mojo's fixed-size array. It owns its elements inline, so Mojo cleans it up and C can fill it through a pointer plus a length.

Both Span and Array are safe to pass to and from C by pointer. Add a length to calls where C needs one.

The following example allocates a 256-byte Array, passes it to C's getcwd(), wraps the filled bytes in a Span, and converts them to a Mojo String:

from std.ffi import external_call, c_char, c_size_t

def main() raises:
    # char *getcwd(char *buf, size_t size); C fills a buffer that Mojo owns.
    comptime CAPACITY = 256
    var buf = Array[c_char, CAPACITY](uninitialized=True)

    var filled = external_call[
        "getcwd", Optional[Pointer[c_char, origin_of(buf)]]
    ](buf.unsafe_ptr(), c_size_t(CAPACITY))
    if not filled:
        raise Error("getcwd failed")

    # C reports no length, so ask for it, then wrap the bytes in a `Span`.
    var length = external_call["strlen", c_size_t](buf.unsafe_ptr())
    var span = Span(
        unsafe_ptr=buf.unsafe_ptr().unsafe_bitcast[Byte](), length=Int(length)
    )
    print(t"{len(span)} bytes: {String(from_utf8=span)}")
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
79 bytes: c017

Spans work with both Mojo and C memory:

Passing strings

C strings are null-terminated byte arrays (char*). Mojo strings are length-prefixed UTF-8.

Convert a Mojo string to a C string

Call as_c_string_span() on a String to ensure null termination, and get a type safe CStringSpan back.

name.as_c_string_span()

The source string must be mutable because as_c_string_span() may append a terminating zero byte. It may also move the string's buffer, so call it once and reuse the result.

Convert a C string to a Mojo string

Use String(unsafe_from_utf8_ptr=...) to copy a null-terminated C string into a Mojo string:

# Copies the bytes; uses `strlen()`.
String(unsafe_from_utf8_ptr=c_string_ptr)

When you already know the length, you can wrap the C bytes in a non-copying, non-owning Span[Byte] and covert that to a Mojo String.

For example, C's strdup() allocates and returns a copy of a string. You can wrap its result in a Span, convert the bytes to a Mojo string, then free the C-owned memory:

var name: String = "Echo"
var cptr = external_call[
    "strdup", Optional[Pointer[c_char, MutUntrackedOrigin]]
](name.as_c_string_span())
if cptr:
    var ptr = cptr.value()
    # Ask C for the length. A Mojo string's `byte_length()` measures the
    # Mojo side, which says nothing about the buffer C returned.
    var length = external_call["strlen", c_size_t](ptr)
    var span = Span(unsafe_ptr=ptr.unsafe_bitcast[Byte](), length=Int(length))
    print(String(from_utf8=span))  # or from_utf8_lossy or unsafe_from_utf8
    external_call["free", NoneType](ptr.unsafe_bitcast[NoneType]())  # free it

Convert Mojo string literals to C strings

String literals can be passed to C APIs that expect a null-terminated char*. Call as_c_string_span() to access the C string:

"libm.so.6".as_c_string_span()

Mojo performs the conversion at compile time and embeds the null-terminated string in the compiled program.

Memory management

Mojo tracks the lifetime of its own memory. C memory has no Mojo value behind it, so there's nothing for Mojo to track. Every allocation that crosses the boundary still belongs to one side, and that side remains responsible for freeing it:

Allocate C memory

C allocators such as malloc return C-owned memory. malloc returns null when the allocation fails. Wrap the return type in Optional:

from std.ffi import external_call, c_size_t

def create_buffer(
    n: c_size_t,
) -> Optional[Pointer[UInt8, MutUntrackedOrigin]]:
    return external_call[
        "malloc", Optional[Pointer[UInt8, MutUntrackedOrigin]]
    ](n)

def main() raises:
    var buf = create_buffer(c_size_t(16))
    if not buf:
        raise Error("malloc failed")
    var ptr = buf.value()
    ptr[unsafe_offset=0] = 42
    print(ptr[unsafe_offset=0])  # 42
    external_call["free", NoneType](ptr.unsafe_bitcast[NoneType]())
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
42

MutUntrackedOrigin tells Mojo not to reason about this pointer's lifetime. It's the opposite of every other origin on this page. Instead of tying the pointer to an owner, it says that no Mojo value owns this memory. You're responsible for keeping it valid and freeing it. You must free it with C's memory management functions, such as free.

Free C memory automatically

Pairing every malloc() with a matching free() by hand is easy to get wrong. A context manager can manage the allocation and release it for you. When the following block exits, __exit__() calls free(), even after a raised error:

from std.ffi import external_call, c_size_t

struct CBuffer:
    var ptr: Pointer[UInt8, MutUntrackedOrigin]
    var size: c_size_t

    def __init__(out self, n: c_size_t) raises:
        self.size = n
        var allocated = external_call[
            "malloc", Optional[Pointer[UInt8, MutUntrackedOrigin]]
        ](n)
        if not allocated:
            raise Error("malloc failed")
        self.ptr = allocated.value()

    def __enter__(self) -> Pointer[UInt8, MutUntrackedOrigin]:
        return self.ptr

    def __exit__(self):
        external_call["free", NoneType](self.ptr.unsafe_bitcast[NoneType]())

def main() raises:
    with CBuffer(c_size_t(1024)) as buf:
        buf[unsafe_offset=0] = 42
        print(buf[unsafe_offset=0])  # 42
    # The buffer is freed here.
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
42

Null returns

C uses null pointers to mean "nothing" or "failed." A Mojo Pointer can't be null, so wrap any "maybe null" return in Optional. The malloc examples you just saw showed this pattern.

Optional's empty case adds nothing to the call and costs nothing to pass:

from std.ffi import external_call, c_char

def main() raises:
    var name: String = "PATH"
    var found = external_call[
        "getenv", Optional[Pointer[c_char, MutUntrackedOrigin]]
    ](name.as_c_string_span())
    if found:
        print(String(unsafe_from_utf8_ptr=found.value()))
    else:
        print(t"{name} is not set")
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
<mojo-env>/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin

Declaring an unwrapped, non-optional Pointer would compile. It would also treat C's null as a valid pointer. Dereferencing results in undefined behavior and will typically crash your program.

Keeping Mojo values alive

Pointers into Mojo memory carry an origin that tracks the value's lifetime. When you derive a pointer from a variable, Mojo keeps the variable alive while the pointer is live. It rejects code that would let the variable die first:

from std.ffi import OwnedDLHandle, c_size_t

def main() raises:
    var proc = OwnedDLHandle()  # No path: opens the current process.
    var c_strlen = proc.get_function[c_size_t]("strlen")

    # The pointer carries `line`'s origin, so `line` outlives the call.
    var line = String("Hello")
    var n = c_strlen(line.as_c_string_span())
    print(t"length of '{line}': {n}")  # 5

    # Refill the same variable and call again. The origin still holds.
    line = "Hello, Mojo!"
    n = c_strlen(line.as_c_string_span())
    print(t"length of '{line}': {n}")  # 12
Show output ✓ Verified · Mojo 1.1.0 (8189361e)
length of 'Hello': 5
length of 'Hello, Mojo!': 12

The pointer's origin ties its lifetime to line. Mojo keeps line alive while C uses the pointer. As a result, you don't need workarounds to extend its lifetime.

Safety

Inside Mojo, the compiler checks types, tracks lifetimes through origins, and refuses code that would use a value after it dies. None of that reaches across the C boundary. C has no origins, no ownership, and no type information Mojo can read, so the compiler emits exactly the call you described and trusts you to have described it correctly.

That makes you the type checker. The C header is the contract, and matching it is your job:

Unsafe operations

Mojo marks operations it can't check for you with an unsafe_ prefix, the same convention used throughout the standard library. This page uses four: unsafe_ptr() to hand C a raw pointer, unsafe_bitcast() to reinterpret one, unsafe_offset= to index past the first element, and String(unsafe_from_utf8_ptr=) to trust bytes C gave you.

Each unsafe_ operation marks a guarantee and responsibility you've taken over from the compiler.

Origins still help wherever a pointer stays inside Mojo's view. Deriving a pointer from a variable, as in line.as_c_string_span(), keeps that variable alive for as long as the pointer lives. That protection ends when C stores the pointer somewhere Mojo can't see.

external_call() and OwnedDLHandle are intentionally low level. Neither validates C signatures or protects you from ABI mismatches. Small declaration mistakes can produce plausible but incorrect results, while others fail only at build time or when you move to a different platform.

Each entry names the API it applies to.

Silently wrong at runtime

Caught at build time

These fail the build, but not always where you'd expect.

Limitations

What the APIs do check

Two guarantees OwnedDLHandle provides that external_call() doesn't:

C type reference

C typestd.ffi aliasEquivalent Mojo typeNotes
int c_int Int32 the most common type by far
short c_short Int16
long c_long depends on target64-bit on Linux and macOS
long long c_long_long Int64 Always 64 bits.
unsigned char c_uchar UInt8
char c_char Int8 signed; you'll mostly see it as char*
unsigned short c_ushort UInt16
unsigned int c_uint UInt32
unsigned long c_ulong depends on targetmatches c_long
float c_float Float32
double c_double Float64
size_t c_size_t UInt for sizes and counts
ssize_t c_ssize_t Int for sizes that can be negative
void* OpaquePointer Pointer[NoneType] see the pointers section, uses origins

Adapted from the Mojo Manual and the MAX GPU programming guides, part of the Modular Platform repository (github.com/modular/modular), Copyright (c) Modular Inc., licensed under the Apache License v2.0 with LLVM Exceptions. Changes were made: reformatted as an accessible XYZ web edition with build-verified example outputs. Mojo and MAX are trademarks of Modular Inc.; this edition is not affiliated with or endorsed by Modular. License: Apache-2.0 WITH LLVM-exception.

These eBooks are a prerelease and are not yet certified conformant with WCAG 2.1 AA or any other accessibility standard. Every page is built against an automated accessibility gate, and our target for the published editions is WCAG 2.2 Level AA. If something is unusable, please tell us.