C Bindings
ruby-bindgen generates Ruby files that use the FFI gem to call C library functions. FFI allows Ruby to directly load and call functions from C shared libraries without writing a C extension. This means:
- No compilation step - Ruby loads the library at runtime
- Easier distribution - No need to compile native code for each platform
- Simpler development - No C code to write or debug
If a library provides a C API then use it!
Configuration
First create a configuration file named ffi-bindings.yaml:
input: ./include
output: ./lib/bindings
format: FFI
project: mylib
match:
- "**/*.h"
library_names:
- mylib
clang:
args:
- -I./include
- -xc
Then to generate bindings run:
mkdir -p ./lib/generated
ruby-bindgen ffi-bindings.yaml
Output
ruby-bindgen generates a project loader file and one content file per header. The loader file requires FFI, loads the shared library, and require_relatives the content files. Each content file reopens the module and defines the structs, enums, callbacks, and functions from that header.
Given a project named mylib that matches mylib.h and mylib_extra.h:
output/
mylib_ffi.rb # loader: require 'ffi', library preamble, require_relatives
mylib.rb # content from mylib.h (reopens module)
mylib_extra.rb # content from mylib_extra.h (reopens module)
The user does require 'mylib_ffi' to load everything.
Loader file (mylib_ffi.rb):
# This file was generated by ruby-bindgen. Please do not edit by hand.
require 'ffi'
module Mylib
extend FFI::Library
def self.library_names
["mylib"]
end
def self.search_names
# Cross-platform library name generation
# Handles .so, .dylib, .dll variants
end
ffi_lib self.search_names
end
require_relative './mylib'
require_relative './mylib_extra'
Content file (mylib.rb):
module Mylib
# Structs
class MyStruct < FFI::Struct
layout :field1, :int,
:field2, :double
end
# Enums
MY_ENUM = enum(
:value_one, 0,
:value_two, 1
)
# Callbacks
callback :my_callback, [:int, :pointer], :void
# Functions
attach_function :my_function, :my_function, [:int, :string], :int
end
Supported Features
ruby-bindgen supports the following FFI features:
- Functions - C functions mapped to Ruby module methods via
attach_function - Structs - C structs mapped to
FFI::Structclasses with proper layouts - Unions - C unions mapped to
FFI::Unionclasses - Enums - C enums mapped to FFI enum types
- Callbacks - Function pointer types for C callbacks
- Typedefs - Type aliases preserved in the generated code
- Forward declarations - Opaque struct types handled correctly
- Global variables - Exported variables via
attach_variable - Constants -
constvariables and simple#definemacros (see Constants and Macros)
String and Pointer Types
C uses char * for both strings and raw memory buffers. ruby-bindgen uses const-qualification and context to choose the correct FFI type:
| Context | const char * |
char * |
|---|---|---|
| Function parameters | :string |
:pointer |
| Function returns | :string |
:pointer |
| Callback returns | :pointer |
:pointer |
| Struct/union fields | :string |
:pointer |
Rationale:
const char *is a read-only string — FFI auto-converts to a RubyString.char *typically indicates a caller-allocated buffer (e.g.,char *buf, size_t buf_size), so:pointeris correct — the caller creates the buffer withFFI::MemoryPointer.new.- Callback returns always use
:pointerregardless of const because FFI cannot manage the lifetime of callback-returned strings.
If a specific function needs a different type mapping, use symbols: overrides: to replace the generated signature.
Struct Pointer Types
When a function parameter is a pointer to a struct, ruby-bindgen generates StructName.by_ref. This is correct for the common case of passing a single struct by pointer:
int proj_get_area_of_use(PJ *obj, double *west, ...);
// → attach_function :proj_get_area_of_use, ..., [:pointer, :pointer, ...], :int
However, .by_ref is wrong when the pointer is actually an array of structs. ruby-bindgen cannot distinguish these cases from the C signature alone — both are just SomeStruct *. Two common patterns:
Array parameters — a count parameter precedes the struct pointer:
PJ *proj_create_conversion(PJ_CONTEXT *ctx, ..., int param_count,
const PJ_PARAM_DESCRIPTION *params);
Here params points to an array of param_count structs. The caller allocates the array with FFI::MemoryPointer and writes structs into it.
Array returns — a function returns a pointer to a statically-allocated or heap-allocated array of structs:
const PJ_OPERATIONS *proj_list_operations(void);
This returns a NULL-terminated array of PJ_OPERATIONS structs, not a single struct. The caller iterates the array by advancing the pointer.
Use symbols: overrides: to fix these:
symbols:
overrides:
proj_create_conversion: "[:pointer, :string, :string, :string, :string, :string, :string, :int, :pointer], :pointer"
proj_list_operations: "[], :pointer"
Examples
The test suite includes bindings generated from some popular C libraries:
| Library | Description |
|---|---|
| PROJ | Coordinate transformation library |
| SQLite | Database engine |
| libclang | C/C++ parsing library |
See test/headers/c for the input headers and test/bindings/c for the generated Ruby bindings.
Library Loading
FFI needs to find and load the C shared library at runtime. The library_names and library_versions configuration options control how ruby-bindgen generates the library search logic.
Library Names
library_names specifies the base names of the shared library. The generated code prepends lib and appends the platform-appropriate suffix:
| Platform | library_names: ["proj"] searches for |
|---|---|
| Linux | libproj, libproj.so.{version} |
| macOS | libproj, libproj.{version}.dylib |
| Windows | libproj, libproj-{version}, libproj_{version} |
Library Versions
C shared libraries use version suffixes that vary by platform and change across releases. library_versions lets you list known version suffixes so FFI can find whichever version is installed.
For example, the PROJ coordinate transformation library has used these version suffixes across releases:
library_names:
- proj
library_versions:
- "25" # PROJ 9.2
- "22" # PROJ 8.x
- "19" # PROJ 7.x
- "17" # PROJ 6.1, 6.2
- "15" # PROJ 6.0
This generates search names like libproj.so.25, libproj.so.22, etc. on Linux, libproj.25.dylib on macOS, and libproj-25 on Windows. FFI tries each name in order until one succeeds. The unversioned libproj is always included as a fallback.
If library_versions is omitted, only the unversioned name is searched. This works on most systems where the package manager creates an unversioned symlink (e.g., libproj.so → libproj.so.25).
Module Name
By default, the generated Ruby module is named after the header file (e.g., proj.h → module Proj). Use the module option to override this, including nested modules:
module: Proj::Api
This generates properly nested module Proj / module Api with correct indentation.
Customization
ruby-bindgen can customize the generated bindings in several ways:
symbols: skip:— exclude specific functions, structs, enums, or typedefs by name or regex patternsymbols: overrides:— replace the generated signature for specific functions when the heuristics pick the wrong FFI typeexport_macros— only include functions marked with specific visibility macrosrename_types— override generated Ruby module/class namesrename_methods— override generated Ruby method names
Version Detection
When symbols.versions has entries, ruby-bindgen generates version-guarded Ruby conditionals and a {project}_version.rb skeleton file. The user implements the version detection method in that file — typically by calling the library's own version API.
Configuration
This example is from proj4rb, Ruby bindings for the PROJ coordinate transformation library. PROJ's API has grown significantly across versions — proj_normalize_for_visualization was added in 6.1.0, proj_cleanup in 6.2.0, and so on.
format: FFI
project: proj
module: Proj::Api
library_names:
- proj
symbols:
skip:
- PJ_INFO # manually defined in version file
- proj_info # manually defined in version file
versions:
# 6.1.0
60100:
- proj_normalize_for_visualization
# 6.2.0
60200:
- proj_cleanup
- proj_as_projjson
- proj_create_crs_to_crs_from_pj
# 8.0.0
80000:
- proj_context_errno_string
The version file calls proj_info() and uses PJ_INFO to compute the runtime version number. Since those symbols are manually defined in the version file, add them to skip so they aren't also generated in the content files.
Generated Output
The generator produces three things:
1. Version guards in content files — version-specific symbols are wrapped in conditionals:
if proj_version >= 60100
attach_function :proj_normalize_for_visualization, ...
end
if proj_version >= 60200
attach_function :proj_cleanup, :proj_cleanup, [], :void
end
2. Version require in the project file (proj_ffi.rb):
require_relative 'proj_version'
require_relative './proj'
3. Version skeleton file (proj_version.rb) — generated once, then user-maintained:
module Proj
module Api
def self.proj_version
# Return the runtime library version as an integer.
# Example: 90602 for version 9.6.2
raise NotImplementedError, "Implement proj_version to return the runtime library version number"
end
end
end
Implementing Version Detection
Replace the skeleton with your library's version API. PROJ provides proj_info() which returns a PJ_INFO struct with major, minor, and patch fields. Since the version file is loaded before the generated content files, define the struct and function here:
module Proj
module Api
class PjInfo < FFI::Struct
layout :major, :int,
:minor, :int,
:patch, :int,
:release, :string,
:version, :string,
:searchpath, :string,
:paths, :pointer,
:path_count, :ulong
end
attach_function :proj_info, :proj_info, [], PjInfo.by_value
def self.proj_version
info = proj_info
info[:major] * 10000 + info[:minor] * 100 + info[:patch]
end
end
end
The version file is loaded before the content files, so proj_version is available when the guards execute. The skeleton is only generated if the file doesn't already exist — your implementation is preserved across re-runs.
Constants and Macros
ruby-bindgen generates Ruby constants from two sources: const variables and #define macros.
Const Variables
Top-level const variables with literal initializers are emitted as Ruby constants:
const int CONST_INT = 10;
static const int STATIC_CONST_INT = 20;
const double CONST_DOUBLE = 3.14;
Generates:
CONST_INT = 10
STATIC_CONST_INT = 20
CONST_DOUBLE = 3.14
Macros
Simple #define macros that expand to a single literal value are emitted as Ruby constants:
#define PROJ_VERSION_MAJOR 9
#define PROJ_VERSION_MINOR 3
#define PROJ_VERSION_PATCH 1
Generates:
PROJ_VERSION_MAJOR = 9
PROJ_VERSION_MINOR = 3
PROJ_VERSION_PATCH = 1
Warning: Only macros with a single literal token are supported. Macros that contain expressions, reference other macros, or take parameters are silently skipped. For example, the PROJ library defines:
#define PROJ_COMPUTE_VERSION(maj, min, patch) \ ((maj) * 10000 + (min) * 100 + (patch)) #define PROJ_VERSION_NUMBER \ PROJ_COMPUTE_VERSION(PROJ_VERSION_MAJOR, PROJ_VERSION_MINOR, \ PROJ_VERSION_PATCH)Neither
PROJ_COMPUTE_VERSION(function-like macro) norPROJ_VERSION_NUMBER(expression macro) will appear in the generated bindings. OnlyPROJ_VERSION_MAJOR,PROJ_VERSION_MINOR, andPROJ_VERSION_PATCHare generated because they each expand to a single integer literal.This is a limitation of libclang's macro representation — macro bodies are stored as raw preprocessor tokens rather than parsed expressions, so there is no way to evaluate them at generation time.
Usage Tips
Since C is procedural rather than object-oriented, you may want to wrap the generated FFI bindings in Ruby classes to provide a more idiomatic API:
require_relative 'generated/mylib'
class MyLibWrapper
def initialize
@handle = Mylib.create_handle()
end
def process(data)
Mylib.process_data(@handle, data)
end
def close
Mylib.destroy_handle(@handle)
end
end