Skip to content

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::Struct classes with proper layouts
  • Unions - C unions mapped to FFI::Union classes
  • 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 - const variables and simple #define macros (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 Ruby String.
  • char * typically indicates a caller-allocated buffer (e.g., char *buf, size_t buf_size), so :pointer is correct — the caller creates the buffer with FFI::MemoryPointer.new.
  • Callback returns always use :pointer regardless 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.solibproj.so.25).

Module Name

By default, the generated Ruby module is named after the header file (e.g., proj.hmodule 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 pattern
  • symbols: overrides: — replace the generated signature for specific functions when the heuristics pick the wrong FFI type
  • export_macros — only include functions marked with specific visibility macros
  • rename_types — override generated Ruby module/class names
  • rename_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) nor PROJ_VERSION_NUMBER (expression macro) will appear in the generated bindings. Only PROJ_VERSION_MAJOR, PROJ_VERSION_MINOR, and PROJ_VERSION_PATCH are 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