Ruby has FFI (Foreign Function Interface) for calling C code. It works, but it's not particularly elegant or fast.
Crystal was designed with C interoperability in mind from day one. You can call C functions, use C structs, link to C libraries, and even inline C code. This gives you access to the massive ecosystem of C libraries while writing Crystal code.
Why would you want this? Performance, existing libraries, hardware access, or because sometimes you just need to get close to the metal.
Crystal's C bindings use the lib keyword to declare external C libraries:
# Binding to standard C library math functions
lib LibC
fun sqrt(value : Float64) : Float64
end
result = LibC.sqrt(16.0)
puts result # => 4.0That's it. You declared a C function and called it from Crystal.
Let's bind to some standard C library functions:
lib LibC
# String length
fun strlen(s : UInt8*) : Int32
# String compare
fun strcmp(s1 : UInt8*, s2 : UInt8*) : Int32
# Get current time
fun time(t : Int64*) : Int64
end
# Use them
str = "Hello"
len = LibC.strlen(str.to_unsafe)
puts len # => 5
# Get current timestamp
timestamp = LibC.time(nil)
puts timestampNotice to_unsafe - it converts a Crystal String to a C char* pointer.
Crystal types map to C types:
| Crystal Type | C Type |
|---|---|
Int8 |
int8_t / char |
Int16 |
int16_t / short |
Int32 |
int32_t / int |
Int64 |
int64_t / long long |
UInt8 |
uint8_t / unsigned char |
UInt16 |
uint16_t / unsigned short |
UInt32 |
uint32_t / unsigned int |
UInt64 |
uint64_t / unsigned long long |
Float32 |
float |
Float64 |
double |
Void |
void |
Bool |
bool (C99+) |
UInt8* |
char* |
Void* |
void* |
C structs map to Crystal structs:
lib LibC
struct TimeSpec
tv_sec : Int64 # seconds
tv_nsec : Int64 # nanoseconds
end
fun clock_gettime(clock_id : Int32, tp : TimeSpec*) : Int32
end
# Use the struct
ts = LibC::TimeSpec.new
LibC.clock_gettime(0, pointerof(ts))
puts "Seconds: #{ts.tv_sec}"
puts "Nanoseconds: #{ts.tv_nsec}"Crystal structs and C structs have compatible memory layouts.
Let's bind to SQLite, a popular C library:
@[Link("sqlite3")]
lib LibSQLite3
type Database = Void*
type Statement = Void*
fun open(filename : UInt8*, db : Database*) : Int32
fun close(db : Database) : Int32
fun exec(db : Database, sql : UInt8*, callback : Void*, arg : Void*, errmsg : UInt8**) : Int32
fun prepare_v2(db : Database, sql : UInt8*, nbyte : Int32, stmt : Statement*, tail : UInt8**) : Int32
fun step(stmt : Statement) : Int32
fun column_text(stmt : Statement, col : Int32) : UInt8*
fun finalize(stmt : Statement) : Int32
SQLITE_OK = 0
SQLITE_ROW = 100
SQLITE_DONE = 101
end
class Database
def initialize(filename : String)
result = LibSQLite3.open(filename, out @db)
raise "Failed to open database" unless result == LibSQLite3::SQLITE_OK
end
def execute(sql : String)
result = LibSQLite3.exec(@db, sql, nil, nil, nil)
raise "Failed to execute SQL" unless result == LibSQLite3::SQLITE_OK
end
def query(sql : String)
results = [] of Array(String)
result = LibSQLite3.prepare_v2(@db, sql, -1, out stmt, nil)
raise "Failed to prepare statement" unless result == LibSQLite3::SQLITE_OK
while LibSQLite3.step(stmt) == LibSQLite3::SQLITE_ROW
row = [] of String
col = 0
loop do
text = LibSQLite3.column_text(stmt, col)
break if text.null?
row << String.new(text)
col += 1
end
results << row
end
LibSQLite3.finalize(stmt)
results
end
def close
LibSQLite3.close(@db)
end
end
# Usage
db = Database.new(":memory:")
db.execute("CREATE TABLE users (id INTEGER, name TEXT)")
db.execute("INSERT INTO users VALUES (1, 'Alice')")
db.execute("INSERT INTO users VALUES (2, 'Bob')")
results = db.query("SELECT * FROM users")
results.each do |row|
puts "ID: #{row[0]}, Name: #{row[1]}"
end
db.closeWe just called SQLite's C API directly from Crystal!
The @[Link] annotation tells the compiler which C library to link:
@[Link("m")] # Link libm (math library)
lib LibM
fun cos(x : Float64) : Float64
fun sin(x : Float64) : Float64
end
@[Link("ssl")] # Link OpenSSL
lib LibSSL
# SSL functions...
end
@[Link("pthread")] # Link pthreads
lib LibPThread
# Thread functions...
endThe linker will find these libraries in standard system locations.
@[Link(ldflags: "-L/usr/local/lib -lmycustomlib")]
lib LibCustom
# Custom library functions...
endCrystal has pointers for C interop:
# Create a pointer to an Int32
x = 42
ptr = pointerof(x)
puts ptr.value # => 42
# Allocate memory
ptr = Pointer(Int32).malloc(10) # 10 Int32s
ptr[0] = 100
ptr[1] = 200
puts ptr[0] # => 100
# Always free manually allocated memory!
ptr.freeCrystal objects → C pointers:
# String to C string
str = "Hello"
c_str = str.to_unsafe # UInt8*
# Array to C array
arr = [1, 2, 3]
c_arr = arr.to_unsafe # Int32*
# Slice to C pointer
slice = Slice(UInt8).new(10)
c_ptr = slice.to_unsafe # UInt8*C string → Crystal String:
lib LibC
fun getenv(name : UInt8*) : UInt8*
end
# Get environment variable
c_value = LibC.getenv("HOME")
unless c_value.null?
home = String.new(c_value)
puts "Home: #{home}"
endSometimes C libraries need callbacks. You can pass Crystal procs to C:
lib LibC
fun qsort(base : Void*, nel : UInt64, width : UInt64,
compar : (Void*, Void*) -> Int32) : Void
end
# Crystal callback
compare = ->(a : Void*, b : Void*) {
x = a.as(Int32*).value
y = b.as(Int32*).value
x <=> y
}
# Use qsort
arr = [5, 2, 8, 1, 9]
LibC.qsort(arr.to_unsafe, arr.size, sizeof(Int32), compare)
puts arr # => [1, 2, 5, 8, 9]The -> syntax creates a C-compatible function pointer.
C unions (all fields share memory) are supported:
lib LibC
union Value
i : Int32
f : Float32
c : UInt8
end
end
val = LibC::Value.new
val.i = 42
puts val.i # => 42
val.f = 3.14_f32
puts val.f # => 3.14
# val.i is now garbage (overlapping memory)C enums become Crystal enums:
lib LibC
enum FileMode
Read = 1
Write = 2
Append = 4
end
fun fopen(path : UInt8*, mode : UInt8*) : Void*
end
# Or use @[Flags] for bitwise enums
@[Flags]
enum Permissions
Read = 1
Write = 2
Execute = 4
end
perms = Permissions::Read | Permissions::WriteBinding to libcurl for HTTP requests:
@[Link("curl")]
lib LibCURL
type CURL = Void*
enum Option
URL = 10002
WRITEFUNCTION = 20011
WRITEDATA = 10001
end
fun init : CURL
fun setopt(curl : CURL, option : Option, ...) : Int32
fun perform(curl : CURL) : Int32
fun cleanup(curl : CURL)
end
# Callback to collect response
response = String::Builder.new
write_callback = ->(ptr : UInt8*, size : UInt32, nmemb : UInt32, userdata : Void*) {
actual_size = size * nmemb
str = String.new(ptr, actual_size)
userdata.as(String::Builder*).value << str
actual_size
}
# Make HTTP request
curl = LibCURL.init
LibCURL.setopt(curl, LibCURL::Option::URL, "https://api.github.com")
LibCURL.setopt(curl, LibCURL::Option::WRITEFUNCTION, write_callback)
LibCURL.setopt(curl, LibCURL::Option::WRITEDATA, pointerof(response))
LibCURL.perform(curl)
LibCURL.cleanup(curl)
puts response.to_s(Note: Crystal has built-in HTTP client - this is just to demonstrate C bindings!)
Many C libraries provide pkg-config metadata:
@[Link(pkg_config: "libxml-2.0")]
lib LibXML2
# XML parsing functions...
endThe compiler will use pkg-config to get the right flags automatically.
Structure for a shard that wraps a C library:
mylib/
├── shard.yml
├── src/
│ ├── mylib.cr # Crystal API
│ └── mylib/
│ ├── lib.cr # C bindings
│ └── wrapper.cr # Safe wrappers
└── spec/
└── mylib_spec.cr
src/mylib/lib.cr (C bindings):
@[Link("mylib")]
lib LibMyLib
fun do_something(x : Int32) : Int32
endsrc/mylib/wrapper.cr (Safe Crystal API):
module MyLib
def self.do_something(x : Int32) : Int32
result = LibMyLib.do_something(x)
raise "Error!" if result < 0
result
end
endsrc/mylib.cr (Public API):
require "./mylib/lib"
require "./mylib/wrapper"Users get a safe Crystal API while you handle the unsafe C bindings internally.
C bindings are unsafe. You can:
- Segfault
- Corrupt memory
- Leak memory
- Invoke undefined behavior
Best practices:
- Wrap C APIs in safe Crystal code
- Validate inputs before passing to C
- Handle errors from C functions
- Document memory ownership
- Test thoroughly
- Bind to a simple C library (like zlib for compression)
- Create a Crystal wrapper that provides a safe API
- Write tests that exercise C function calls
- Use pkg-config to link a library
- Create a callback from C to Crystal
- The
libkeyword declares C libraries - C functions are bound with
fun - C structs map to Crystal structs
@[Link]specifies which library to link- Pointers bridge Crystal and C memory
to_unsafeconverts Crystal objects to C pointers- C bindings are powerful but require care
You've now seen how Crystal interacts with C code. Next, we'll explore macros - Crystal's compile-time metaprogramming system. You'll learn how to generate code, create DSLs, and achieve Ruby-like magic at compile time instead of runtime. This is where Crystal's philosophy of "flexible syntax, fast execution" really shines.
Continue to Chapter 09 - Macros: The Metaprogramming You Thought You Lost →