Skip to main content

08. Native C extension gems

This note documents how the MEP-76 bridge handles gems that include native C (or C++) extensions. These gems require compilation at install time and introduce platform-specific artifacts that complicate bridging.

What a native extension gem is

A native extension gem contains an ext/ directory with an extconf.rb script and one or more .c or .cpp source files. When the gem is installed via gem install or bundle install, Ruby runs extconf.rb (which invokes mkmf to produce a Makefile), then compiles the C sources against the current Ruby headers. The result is a .so (Linux), .dylib (macOS), or .dll (Windows) that Ruby loads via require.

Common native extension gems:

GemC dependencyPurpose
nokogirilibxml2 + libxsltHTML/XML parsing
pglibpq (PostgreSQL client)PostgreSQL adapter
sqlite3SQLite3 C librarySQLite adapter
mysql2libmysqlclientMySQL adapter
msgpackmsgpack-cMessagePack serialisation
fast_jsonparsersimdjsonHigh-speed JSON parsing
oj(vendored C)Optimised JSON
bcrypt-rubyOpenBSD bcryptPassword hashing

Detection strategy

The bridge cannot run gem install at lock time (that would require a Ruby runtime, a native toolchain, and network access). Instead it detects native extensions by inspecting the gemspec field spec.extensions:

# A native extension gem has a non-empty extensions array:
spec.extensions = ["ext/nokogiri/extconf.rb"]

The bridge fetches the gemspec from the compact index at lock time. If extensions is non-empty, the gem is flagged as native. The bridge then applies the three-strategy resolution order described below.

The detection happens in package3/ruby/index/native.go and runs as part of the mochi pkg lock dependency resolution pass.

Three resolution strategies

Strategy 1: pre-built binary gem

RubyGems.org hosts platform-specific binary gems alongside the source gem. The binary gem has a platform suffix in its filename:

nokogiri-1.16.2-x86_64-linux.gem
nokogiri-1.16.2-aarch64-linux.gem
nokogiri-1.16.2-x86_64-darwin.gem
nokogiri-1.16.2-arm64-darwin.gem
nokogiri-1.16.2-x64-mingw-ucrt.gem

The binary gem's spec.platform field is set to the target platform string. The bridge selects the binary gem matching the host platform by querying the compact index for the platform-qualified version entry:

# compact index info line for a binary version:
1.16.2-x86_64-linux |checksum:sha256hex,ruby:>= 2.7

The platform selection logic in package3/ruby/index/compact.go maps Go's runtime.GOOS / runtime.GOARCH to the RubyGems platform string:

Go GOOS/GOARCHRubyGems platform
linux/amd64x86_64-linux
linux/arm64aarch64-linux
darwin/amd64x86_64-darwin
darwin/arm64arm64-darwin
windows/amd64x64-mingw-ucrt

MEP-76 phase 12 implements binary gem selection.

Strategy 2: pure-Ruby alternative

Many native extension gems have a pure-Ruby fallback gem. The bridge maintains a curated mapping in package3/ruby/index/native.go:

Native gemPure-Ruby alternative
json (C ext)json_pure
msgpackmsgpack-pure (community)
bcrypt-ruby(no pure alternative; skipped)
psych (YAML)psych ships pure-Ruby fallback in Ruby stdlib

When no binary gem is available for the current platform but a pure-Ruby alternative exists, the bridge substitutes the alternative and emits a warning:

WARN: no binary gem for [email protected] on arm64-darwin
Substituting pure-Ruby alternative: [email protected]
Note: pg_pure has known performance limitations; binary gem preferred.

The substitution is recorded in mochi.lock so that subsequent mochi pkg lock runs are deterministic.

Strategy 3: skip with SkipReport

If no binary gem is available and no pure-Ruby alternative exists, the gem is skipped with a NativeExtensionSkip report:

SKIPPED: [email protected] (native extension, no binary gem for arm64-darwin, no pure alternative)
To enable source compilation, add to mochi.toml:
[ruby.native]
allow_source_build = true

The skip report is written to <workdir>/ruby_wrap/<gem>/skip_report.txt alongside any type-mapping skips.

The [ruby.native] manifest section

[ruby.native]
# Default: prefer binary gem only; skip if no binary available.
allow_source_build = false

# Platforms to resolve binary gems for.
# Default: auto-detect from build host.
platforms = ["x86_64-linux", "arm64-darwin", "x64-mingw-ucrt"]

When allow_source_build = true, the bridge attempts source compilation using the system C toolchain (cc, make). This requires:

  • Ruby headers (e.g., ruby-dev package on Debian/Ubuntu)
  • The native library development headers (e.g., libpq-dev for pg, libsqlite3-dev for sqlite3)
  • make and a C compiler

Source builds are disabled by default because they are fragile in containerised CI environments and can produce non-reproducible binaries. The preferred path is always a binary gem.

Nokogiri case study

nokogiri is the most widely used native extension gem. Its binary gem situation as of 2026:

nokogiri-1.16.2 # source gem (requires libxml2, libxslt to compile)
nokogiri-1.16.2-x86_64-linux # pre-built, bundles libxml2 + libxslt
nokogiri-1.16.2-aarch64-linux # pre-built
nokogiri-1.16.2-x86_64-darwin # pre-built
nokogiri-1.16.2-arm64-darwin # pre-built
nokogiri-1.16.2-x64-mingw-ucrt# pre-built (Windows)
nokogiri-1.16.2-java # JRuby variant (not bridged in MEP-76 v1)

Binary gems for nokogiri bundle the C libraries statically, so no system libxml2/libxslt is required. The bridge selects the platform-specific binary gem automatically.

RBS coverage for native extension gems

RBS availability varies significantly across native gems:

GemRBS statusSource
nokogiriBundled in gem (added in 1.14)lib/nokogiri/**/*.rbs inside the gem
pgPartial (via gem_rbs_collection)gem_rbs_collection repo; incomplete as of 2026
sqlite3Partial YARD docs onlyNo .rbs files; the bridge converts YARD via rbs-yard (best-effort)
mysql2NoneNo RBS; bridge emits a SkipReport for all mysql2 items
msgpackBundled in gem (added in 1.7)Good coverage of the core serialisation API
ojNoneNo RBS; SkipReport generated
bcrypt-rubyNoneSkipReport generated; user must hand-author bindings

For gems with no RBS coverage, the bridge generates a SkipReport for the entire gem. The gem may still be installed (if a binary is available), but no Mochi bindings are emitted.

Cross-references