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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
It’s wrapped in a Julia function with a Julia-friendly signature and a docstring. Callers never see
@ccall.It converts at the boundary —
Int(...)turns theCintinto an ordinary JuliaIntso nothing downstream has to think about C types.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.
Try it yourself#
Use
@ccallto callgetpid()and print your Julia process’s ID. Check it against your operating system’s process list.Call
strlenon a Julia string and confirm the result matcheslength. Then try it with a string containing an emoji, and explain the difference to yourself.Create a
Ref{Cint}, set a value, read it back with[].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.