Type-safe Ruby native extensions written in Zig.
zig.rb provides a high-level API for creating Ruby extensions in Zig with compile-time type safety, automatic memory management, and idiomatic Zig patterns. The library wraps Ruby's C API with zero-cost abstractions that prevent common errors while maintaining full performance.
- Type-safe Value conversions - Compile-time checked conversions between Ruby and Zig types
- Automatic memory management - RubyAllocator integrates with Ruby's GC
- Class and module definitions - Define Ruby classes with Zig structs
- Method binding - Automatic Ruby method wrappers with arity checking
- Full Ruby type support - Fixnum, Bignum, Float, String, Array, Hash, Symbol
- Format support - Direct integration with Zig's std.fmt for Ruby values
- Error handling - Type-safe error propagation between Ruby and Zig
Note: The complete Ruby C API remains available via the raw bindings exposed under rb.crb.*.
Add zig.rb to your build.zig.zon:
.dependencies = .{
.zig_rb = .{
.url = "https://github.com/yourusername/zig.rb/archive/main.tar.gz",
.hash = "...",
},
},
const rb = @import("zig_rb");
const Value = rb.Value;
const Module = rb.Module;
fn add(_: Value, a: Value, b: Value) Value {
// first parameter is module self
const a_int = a.toInt(i64) catch 0;
const b_int = b.toInt(i64) catch 0;
return Value.from(a_int + b_int);
}
export fn Init_myextension() void {
rb.init();
const mod = Module.define("MyExtension");
mod.defineFunction(add, "add");
}
const Counter = struct {
const Self = @This();
count: i64,
pub const ruby_type = TypedDataClass.createDataType(Self, "Counter", RubyType);
pub const RubyType = struct {
pub fn alloc(_: Value, allocator: std.mem.Allocator) ?*Self {
const ptr = allocator.create(Self) catch return null;
ptr.* = .{ .count = 0 };
return ptr;
}
pub fn mark(_: *Self) void {}
pub fn free(self: *Self, allocator: std.mem.Allocator) void {
allocator.destroy(self);
}
};
pub const InstanceMethods = struct {
pub fn increment(self: *Self) Value {
self.count += 1;
return Value.from(self.count);
}
pub fn get_count(self: *Self) Value {
return Value.from(self.count);
}
};
pub const Constants = struct {
pub const VERSION = "1.0.0";
pub const MAX_VALUE = 1000000;
};
};
export fn Init_counter() void {
rb.init();
_ = TypedDataClass.defineFromStructs("Counter", Counter);
}
Ruby usage:
counter = Counter.new
counter.increment # => 1
counter.increment # => 2
counter.get_count # => 2
Counter::VERSION # => "1.0.0"
// Integers (Fixnum and Bignum)
const i = value.toInt(i64) catch 0;
const u = value.toInt(u32) catch 0;
// Floats
const f = value.toFloat(f64) catch 0.0;
// Strings
const str = value.toString() catch "";
// Booleans
const b = value.toBool() catch false;
// Type checking
if (value.isNil()) { ... }
const type_tag = value.getType(); // Returns Value.Type enum
// From primitives
const v1 = Value.from(42); // Fixnum
const v2 = Value.from(3.14); // Float
const v3 = Value.from(true); // TrueClass
const v4 = Value.from("hello"); // String
// Explicit constructors
const v5 = Value.newInt(i64, 100);
const v6 = Value.newFloat(2.718);
const v7 = Value.newString("world");
// Special values
const nil_val = Value.nil;
const true_val = Value.true;
const false_val = Value.false;
const rb = @import("zig_rb");
const Array = rb.Array;
const Value = rb.Value;
fn array_sum(_: Value, rb_array: Value) Value {
const array = Array.fromValue(rb_array);
var sum: i64 = 0;
var iter = array.iterator();
while (iter.next()) |elem| {
sum += elem.toInt(i64) catch 0;
}
return Value.from(sum);
}
Ruby values integrate with Zig's std.fmt:
const value = Value.from(42);
std.debug.print("Value: {}\n", .{value}); // Calls Ruby's inspect
const Error = rb.Error;
fn divide(_: Value, a: Value, b: Value) Value {
const a_int = a.toInt(i64) catch {
Error.raiseTypeError("first argument must be an integer");
};
const b_int = b.toInt(i64) catch {
Error.raiseTypeError("second argument must be an integer");
};
if (b_int == 0) {
Error.raiseArgumentError("division by zero");
}
return Value.from(@divTrunc(a_int, b_int));
}
Use the provided build utilities for seamless integration:
const std = @import("std");
pub fn build(b: *std.Build) void {
const ruby = @import("zig_rb").ruby;
// Get Ruby configuration
const ruby_config = try ruby.getConfig(b);
// Create extension
const ext = ruby.addExtension(b, &ruby_config, .{
.name = "myextension",
.root_module = my_module,
});
// Install to lib/ for development
try ruby.installExtensionToLib(b, &ruby_config, ext, "myextension");
}
- Value - Wrapper for Ruby VALUE with type-safe conversions
- TypedDataClass - Define Ruby classes backed by Zig structs
- Module - Define Ruby modules and singleton methods
- Array - Ruby Array operations
- Hash - Ruby Hash operations
- Error - Raise Ruby exceptions
- RubyAllocator - std.mem.Allocator that uses Ruby's memory allocation system
| Fixnum | i8 to i64, u8 to u64 | Native integer types |
| Bignum | std.math.big.int.Managed | Arbitrary precision |
| Float | f32, f64 | IEEE 754 floats |
| String | []const u8 | UTF-8 byte slices |
| Symbol | Value | Via Value.toSymbol() |
| Array | Array | Indexed collection |
| Hash | Hash | Key-value store |
| true/false | bool | Boolean values |
| nil | Value.nil | Null value |
See the example/ directory for a complete gem project demonstrating:
- Standard Ruby gem structure
- Build system integration
- Extension compilation
- Testing with minitest
- Gem packaging
- Zig 0.15.2 or later
- Ruby 2.6 or later
- C compiler (for Ruby headers)
MIT
Contributions welcome. Please ensure all tests pass before submitting PRs.
- Conversion helpers between Zig maps and Ruby Hash values
- Custom Ruby error type wrappers
.png)


