whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Writing a Ruby Gem That Wraps a C Library Through FFI

When a Ruby project hits the limits of pure-Ruby performance, the usual reflex is to reach for a C extension or jump to a different language entirely. There is a third path that sits comfortably between those choices, and it works particularly well when wrapping an existing C library rather than writing one. That path is FFI, the Foreign Function Interface, and it lets a Ruby gem call into shared libraries without compiling against MRI internals.

For developers working out of Melbourne or Brisbane co-working spaces, where teams regularly pull in C libraries for image processing, audio work, or hardware integration, FFI can shave weeks off a project timeline. You skip the extconf.rb dance, skip precompiled binaries for every Ruby version, and can iterate on the binding almost as fast as plain Ruby code.

Approach Compile Step Required Cross-Ruby Version Support Build Complexity
FFI binding No Yes (MRI, JRuby, TruffleRuby) Low
C extension with mkmf Yes Per Ruby ABI High
SWIG wrapper Yes Per target language Medium
Pure Ruby port No Yes Variable

Why FFI Beats Native Extensions for Ruby C Wrapping

The traditional route for talking to a C library from Ruby involves writing a C extension, generating a Makefile through mkmf, and compiling against the headers of whichever Ruby interpreter you target. It works, but it locks you into a specific Ruby version and forces you to maintain native binaries across platforms. When a new Ruby release ships every Christmas and half the team is on Macs while CI runs on Linux, the rebuild tax adds up.

FFI sidesteps most of that. You declare the C signatures in Ruby, point the gem at a .so, .dylib, or .dll shipped with the operating system or vendored alongside your code, and calls happen through a thin runtime. The same wrapper often runs on JRuby and TruffleRuby with no changes, a quiet win for teams shipping SaaS products across runtimes.

There is a small performance cost on MRI because every FFI call crosses the Ruby-to-C boundary through a more indirect path than a native extension. For tight loops crunching millions of numbers, that cost matters. For the common case of wrapping a library that already does the heavy lifting, you keep the speed of C while gaining the ergonomics of Ruby, and a useful resource on external design approaches explores similar patterns elsewhere.

Setting Up the Gem Skeleton and Dependencies

A useful starting point is the standard bundle gem command, followed by deliberate edits. You want an ffi dependency in your .gemspec, a lib/ directory for the binding code, and a vendor/ directory if you plan to ship the shared library alongside the gem. Many Australian maintainers keep the C library sources in a submodule and build them in a Rakefile task.

Inside lib/your_gem.rb, you typically require FFI, attach the shared library, and expose a clean Ruby API on top. The attach_function method takes the return type, the C function name, and an array of argument types. The signatures read almost like C, which means a developer who already knows the library can write the binding without a deep dive into MRI internals.

A reasonable layout: lib/your_gem/ffi_bindings.rb for raw declarations, lib/your_gem/wrappers.rb for the friendly Ruby layer, and lib/your_gem/errors.rb for translated C error codes. Splitting the binding from the wrapper keeps the messy bits in one place.

Mapping C Functions and Memory Layout

Once the skeleton is in place, the real work begins: matching each C function signature to an FFI declaration. FFI exposes common primitive types directly, including :int, :uint, :double, :pointer, and :string. Structs are defined with FFI::Struct subclasses, and you describe layouts field by field with their C types and offsets.

The trickiest parts are pointers, callbacks, and arrays. A pointer can be created from a Ruby string with FFI::MemoryPointer.from_string, allocated for a fixed size with FFI::MemoryPointer.new, or returned from the C library and managed through FFI::AutoPointer. AutoPointer pairs the pointer with a release function, preventing leaks when the C library owns the memory.

Callbacks deserve special care. When a C library expects a function pointer, describe it with FFI::Function, supply the argument and return types, and pass a Ruby block. The block becomes a callable C function for the call's duration. You can raise Ruby exceptions inside it, but the C side sees whatever the function declared as the return type, so keep the contract honest.

Handling Structs, Callbacks and Pointers

A typical use case is a C struct that holds configuration for a hardware device or media decoder. Wrapping it cleanly means exposing accessors for each field and providing Ruby-friendly constructors. A Config struct with two integers and a buffer can sit inside a Ruby class that accepts keyword arguments, validates ranges, and touches the underlying struct layout only when needed.

Memory safety is the silent killer of FFI gems. A pointer that the C library allocates and returns to Ruby must either be released explicitly or wrapped in FFI::AutoPointer. A pointer that Ruby allocates and hands to the C library stays alive for as long as the C function needs it, fine for synchronous calls but risky if the C side retains a reference. Document the ownership contract in your README, and if you want to see a similar native-interop pattern on the macOS side, the cocoa-using-nstrackingarea notes walk through the same kind of struct-and-callback dance.

For multi-threading, FFI on MRI holds the Global VM Lock like any other Ruby C call, so you will not get parallelism from threads alone. JRuby and TruffleRuby can release the GVL around FFI calls, one reason audio and graphics gems run faster there. Studios in Adelaide working with custom sensor rigs often note which interpreters give them the throughput they need.

Building a Test Suite That Actually Runs the Native Code

A wrapper gem without tests is a future bug report. FFI makes testing easy in one sense: the binding code is plain Ruby, so you can exercise it with RSpec or Minitest without any special harness. The catch is that you also need to confirm the native library behaves the way your wrapper assumes.

A practical approach is to split tests into two layers. The first covers pure Ruby wrappers with mocks for the FFI calls, running fast on any developer machine. The second hits the real shared library and runs slowly, gated behind an environment variable so CI can opt in. Many Australian teams wire the slow suite into a nightly job on a beefy runner and keep the fast suite green for every pull request.

For gems that wrap system libraries, document the minimum version you support. On macOS, that often means Homebrew; on Linux, the distro package; on Windows, a vendored DLL. A Gem::Dependency declaration plus a check in the gem itself can raise a helpful error during require instead of a cryptic segfault later.

Packaging and Publishing the Gem Safely

Before you push to RubyGems, run through a short checklist. Confirm your .gemspec does not include compiled .so files for the C library unless you intend to vendor them. Decide whether the gem should fall back to a system installation or require the library to be present, and make that decision explicit in the README. Sign the release with gem sign if your audience expects it; corporate buyers in Sydney and Canberra often look for signed artifacts as part of procurement.

Publishing itself is gem push, but the polish matters. Write a changelog, tag the release, and update the version following SemVer. A breaking change in the wrapper API deserves a major bump, while a fix to a memory leak or a new optional method deserves a patch or minor bump. Readers rely on the version number to know what they are upgrading into.

If your wrapper is for an internal library at a Perth-based fintech or a Melbourne robotics shop, you might also publish it to a private gem server. Geminabox and Gemstash both serve that need and integrate cleanly with Bundler through a custom source line in the Gemfile. Keeping internal gems off the public registry is a small privacy win.

Common Pitfalls and How Aussie Teams Handle Them

One common mistake is forgetting to load the shared library before attaching functions. FFI does not raise until the first call, which can make the failure look like a bug in the C library rather than a missing ffi_lib line. The fix is straightforward, but the habit of asserting the library loads at require time saves hours later.

Another pitfall is mismatched struct layouts. If a field offset is wrong, you will read garbage values and chase phantom bugs through your wrapper. Always cross-check against the C header with the same packed semantics and field order. Tools like pahole on Linux can dump struct layouts for comparison, and a five-minute check will save a five-hour debugging session.

Finally, do not underestimate documentation. A wrapper gem is only as good as the examples that show users how to call it. A short README with three working snippets — one trivial, one realistic, one showing error paths — will do more for adoption than any amount of internal polish. If you want a deeper dive into a related topic, the using-illustrator-s-blend-tool-to-create-complex-svg-gradients guide covers a different corner of the author's stack.

Take what you have learned and start small. Pick a C library you already use, wrap three or four of its core functions, and publish version 0.1.0 to RubyGems or your private server this week. The feedback from real users will teach you more than another month of planning, and the next iteration will be easier because the skeleton is already in place. If you are keen as to start today, spend an arvo wiring up the FFI binding and another day writing the friendly Ruby layer on top. Drop a comment on the journal if you ship something interesting — the author enjoys seeing what readers build, and the occasional war story makes the next post easier to write.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames