Calling C, and Working with Pointers#

This chapter is the one place in the book where we take the safety rails off. Julia can call C and Fortran libraries with no wrapper code, no glue layer, and no overhead — and it can hand out raw pointers to its own memory. That’s a genuine superpower for a language this high-level, and it’s also the corner where you can crash your process.

I’ll be clear about the split: almost every Julia programmer uses ccall occasionally and raw pointers almost never. Read this chapter so you can understand the code you’ll encounter, and so you know the escape hatch is there. Then go back to writing ordinary Julia.

Why this exists at all#

Decades of numerical computing are written in C and Fortran. BLAS, LAPACK, FFTW, HDF5, SQLite, libcurl — the foundations of scientific computing are compiled libraries that nobody is going to rewrite. Julia’s approach isn’t to reimplement them or generate binding code; it’s to call them directly, as a language feature.

Here’s the entire mechanism:

julia> ccall(:clock, Int32, ())
1234567

That called the C standard library’s clock() function. No header parsing, no build step, no .so shim.

The anatomy of ccall#

@ccall library.function_name(arg1::ArgType1, arg2::ArgType2)::ReturnType

The modern @ccall macro reads much better than the older four-argument ccall form you’ll see in existing code. An example with actual arguments:

julia> @ccall sqrt(16.0::Cdouble)::Cdouble
4.0

Every argument carries a C type, and Julia handles the conversion. The C-compatible types are named to match:

Julia

C

Cint, Cuint

int, unsigned int

Clong, Culong

long, unsigned long

Cfloat, Cdouble

float, double

Cchar

char

Cstring

char* (null-terminated)

Cvoid

void

Ptr{T}

T*

Use these names rather than Int32 directly. Cint is Int32 on every platform you’re likely to meet, but on the one where it isn’t, your code still works.

Calling into a named library#

# Get the size of a file using libc's stat, conceptually:
@ccall strlen("hello world"::Cstring)::Csize_t     # 11

For a library other than libc, name it:

@ccall "libm".cos(1.0::Cdouble)::Cdouble

In real packages, the library is provided by a JLL package — a Julia package that ships pre-built binaries for every platform, produced by the BinaryBuilder system. If you want to wrap a C library for the ecosystem, you don’t ask users to install it; you build a JLL and depend on it. That’s why installing a Julia package that wraps a huge C library “just works” on Windows, macOS, and Linux without a compiler.

Passing arrays to C#

Julia arrays are contiguous blocks of memory with a C-compatible layout, so passing one to a C function costs nothing:

x = [1.0, 2.0, 3.0, 4.0]

# BLAS's dnrm2 computes the Euclidean norm
n = @ccall "libopenblas".dnrm2_64_(
    length(x)::Ref{Int64},
    x::Ptr{Float64},
    1::Ref{Int64}
)::Float64

The array is passed as a pointer to its first element. No copy, no marshalling.

Warning

The garbage collector doesn’t know about C This is the one genuinely dangerous thing in this chapter.

If you pass a pointer to C and the C function stores it for later, Julia’s garbage collector may free that memory while C still holds the pointer. You get a crash, or worse, silent corruption.

For the duration of a single @ccall, Julia keeps the arguments alive automatically. If a pointer outlives the call, you must protect it yourself with GC.@preserve:

GC.@preserve x begin
    p = pointer(x)
    # ... C holds p during this block ...
end

If you never write a C callback that stashes a pointer, you’ll never need this. If you do, forgetting it is a Tuesday afternoon you won’t get back.

Pointers#

Julia has two pointer types, and the naming tells you everything:

  • Ptr{T} — a raw, unmanaged address. Unsafe. For talking to C.

  • Ref{T} — a managed reference the garbage collector knows about. Safe. For output parameters.

Ref is what you use when a C function wants to write into a variable you pass:

out = Ref{Cint}(0)
# C function writes into out
# ... @ccall some_c_function(out::Ref{Cint})::Cvoid
out[]                 # read the value back

The [] dereferences it. That’s the same syntax you’d use on a zero-dimensional array, which is essentially what a Ref is.

The genuinely unsafe operations are, mercifully, named to tell you so:

unsafe_load(p, i)          # read from a raw address
unsafe_store!(p, v, i)     # write to a raw address
unsafe_wrap(Array, p, n)   # treat a C-allocated block as a Julia array
unsafe_string(p)           # build a String from a char*

Julia’s naming convention here is a small piece of good design worth noticing: every function that can segfault your process has unsafe_ in its name. You cannot use one by accident, and a code reviewer can find all of them with grep.

Memory: the newer, lower-level building block#

Note

New in Julia 1.11 Julia 1.11 introduced Memory{T}, a fixed-length, low-level block of memory that now sits underneath Array. Before this, Array was a special compiler-blessed type; now most of Array’s implementation is ordinary Julia code built on Memory.

You will rarely use Memory directly. It matters for two reasons: it’s another step in Julia’s project of implementing the language in itself, and if you’re reading array internals in the standard library, you’ll now see Memory at the bottom of the stack rather than compiler magic.

A complete, realistic example#

Let’s wrap a real C function properly — the shape of code you’d actually ship.

"""
    system_memory_page_size()

Return the operating system's memory page size in bytes.
"""
function system_memory_page_size()
    if Sys.isunix()
        return Int(@ccall getpagesize()::Cint)
    else
        error("not implemented on this platform")
    end
end
julia> system_memory_page_size()
4096

Three things that make this production-quality rather than a demo:

  1. It’s wrapped in a Julia function with a Julia-friendly signature and a docstring. Callers never see @ccall.

  2. It converts at the boundaryInt(...) turns the Cint into an ordinary Julia Int so nothing downstream has to think about C types.

  3. It handles the platform rather than crashing mysteriously on Windows.

That’s the pattern for every C wrapper: a thin, safe Julia surface over the unsafe call, with conversion at the edge.

Compiling Julia into a shareable binary#

The other direction is worth knowing about, because it’s the answer to “can I actually deploy this?”

Historically, shipping Julia meant shipping the whole runtime — hundreds of megabytes. Two developments changed that:

  • PackageCompiler.jl builds a custom system image or an app bundle with your package baked in, removing the compile latency at startup.

  • --trim (experimental, Julia 1.12) strips code that isn’t statically reachable from your entry point. In the Julia team’s testing, a small program went from 206 MB to 1.6 MB. It’s driven through JuliaC.jl, which gives you a CLI.

The catch on --trim is real: your code must have no dynamic dispatch reachable from the entry point, or trimming isn’t provably safe and compilation errors out. In practice that means writing type-stable code, which — conveniently — is exactly what the next chapter is about.

I want to flag why this matters to me personally. The thing that defeated me during the pandemic wasn’t writing the model; it was getting it onto hardware without a rewrite. A Julia binary you can ship is the last piece of that puzzle. It’s experimental today and I wouldn’t bet a production deployment on it yet, but the direction is unmistakable.

Try it yourself#

  1. Use @ccall to call getpid() and print your Julia process’s ID. Check it against your operating system’s process list.

  2. Call strlen on a Julia string and confirm the result matches length. Then try it with a string containing an emoji, and explain the difference to yourself.

  3. Create a Ref{Cint}, set a value, read it back with [].

  4. Look up a JLL package on juliahub.com (try Zlib_jll), add it, and see what artifacts it ships.

Solutions#

# 1.
@ccall getpid()::Cint

# 2.
s = "hello"
@ccall strlen(s::Cstring)::Csize_t      # 5, same as length(s)

s2 = "hi🔥"
length(s2)                              # 3 characters
@ccall strlen(s2::Cstring)::Csize_t     # 6 bytes — C counts bytes, Julia counts characters

# 3.
r = Ref{Cint}(42)
r[]                                     # 42
r[] = 7
r[]                                     # 7

Exercise 2 is a nice little lesson in why Cstring conversions need care.

You’ve now seen the bottom of the language. Next we come back up a level to something that affects code you write every day: mutation, copies, and who owns what.