Call C functions from Spinel Ruby programs. No extension compiler, no
require "ffi": declarations go straight into the source and the AOT
compiler generates direct C call sites with the right externs and
linker flags.
module LibC
ffi_func :strlen, [:str], :size_t
ffi_func :getpid, [], :int
end
puts LibC.strlen("hello, world") # 12
puts LibC.getpidCompile and run:
./spinel prog.rb && ./proglibc and libm are always linked; anything else needs ffi_lib.
All FFI declarations go inside a module body. The module name becomes
the namespace for the functions (RAY.InitWindow, LibC.strlen, …).
Declares that this module needs -lname on the link command line. May
appear multiple times per module.
module SQL
ffi_lib "sqlite3"
endDeclares cflags (include dirs, defines, link-search paths) needed for this module's externs. Rarely needed — externs use standard C types only, so headers don't have to be included in the generated code — but useful when a library is installed somewhere non-standard.
ffi_cflags "-I/usr/local/include"
ffi_cflags "-Wl,-rpath,/usr/local/lib"Declares a C function callable as Module.name(...).
ffi_func :sqlite3_open, [:str, :ptr], :int
ffi_func :sqlite3_close, [:ptr], :int
ffi_func :sqlite3_exec, [:ptr, :str, :ptr, :ptr, :ptr], :int
ffi_func :sqlite3_errmsg, [:ptr], :strRecognized type specs:
| spec | C type | Spinel type |
|---|---|---|
:int |
int |
int |
:uint32 |
uint32_t |
int |
:int32 |
int32_t |
int |
:uint16 |
uint16_t |
int |
:int16 |
int16_t |
int |
:uint8 |
uint8_t |
int |
:int8 |
int8_t |
int |
:size_t |
size_t |
int |
:long |
long |
int |
:float |
float |
float |
:double |
double |
float |
:bool |
int |
bool |
:str |
const char * |
string |
:ptr |
void * |
ptr |
:void |
void |
void (return only) |
All integer types collapse to mrb_int (int64) inside Spinel and are
cast to the declared C type at the call boundary. Floats collapse to
double the same way.
Declares an integer constant accessible as Module::NAME. Pure
convenience — the value is inlined at use sites like any other Ruby
integer constant.
ffi_const :SQLITE_OK, 0
ffi_const :SQLITE_ROW, 100
ffi_const :SQLITE_DONE, 101Declares a static size-byte buffer, accessible as Module.name
returning a :ptr. Useful as scratch space or as an out-parameter for
functions like sqlite3's sqlite3_open, which writes the database
handle into a caller-supplied sqlite3 **.
ffi_buffer :db_out, 8
SQL.sqlite3_open(":memory:", SQL.db_out)
db = SQL.read_ptr(SQL.db_out) # the actual sqlite3 *Lifetime: static. The buffer lives for the whole program.
Declares a field reader: Module.name(buf) returns the value at
offset bytes into buf. Handy for poking into C structs when you
only need a few fields, or for reading back what a C function wrote
into a buffer you handed it.
# sqlite3_open(path, ppDb) writes the new db handle into *ppDb.
# Pull the pointer out of our scratch buffer at offset 0.
ffi_read_ptr :read_ptr, 0
db = SQL.read_ptr(SQL.db_out)No ffi_write_* in the MVP — the assumption is that a C function is
the one writing into the buffer; Ruby just reads back.
:ptr maps to C void *. Values of this type are not GC-tracked:
the Spinel garbage collector never follows them and never frees them.
Foreign memory is the user's responsibility.
Two consequences worth knowing:
- Call destroy functions explicitly. Nothing calls
sqlite3_close,sqlite3_finalize, orfree()for you. - Strings passed into C are only valid for the duration of the
call. Spinel strings are GC-managed; if a C function stashes the
pointer somewhere and the string becomes unreachable afterward, a
later GC cycle will free it out from under the C code. If you need
a string to outlive the call, copy it into an
ffi_bufferfirst.
ptr values compare equal to nil when the pointer is NULL:
db = SQL.read_ptr(SQL.db_out)
if db == nil
puts "could not open database"
endThe codegen emits marker comments into the generated C:
/* SPINEL_LINK: -lsqlite3 */
/* SPINEL_CFLAGS: -I/usr/local/include */The spinel wrapper script scrapes these with sed and appends them
to the cc invocation. If you want to override (e.g. static linking or
a custom lib path), use -c to stop at C and drive the linker yourself.
The MVP covers scalars, strings, opaque pointers, integer constants, raw byte buffers, and simple struct-field reads. Not supported yet:
- No struct declarations. Use
ffi_buffer+ffi_read_*for the handful of fields you need. - No callbacks / Ruby-to-C function pointers.
- No variadic C functions (
printf(...)). Use Spinel's built-inprintfif you want formatted output. - No
ffi_write_*— can't write struct fields from Ruby. Pass a buffer to a C function that writes it for you. - Pointers can't enter polymorphic values. Don't put a
:ptrinto apoly_arrayor a genericHash; keep them as plain locals or wrap them in a class with aptr-typed ivar.
Runnable examples live under examples/ffi/:
examples/ffi/libm/— libc / libm smoke (cos, sqrt, pow, strlen, getpid)examples/ffi/sqlite/— blog system (posts, tags, comments) on sqlite3
Each subdirectory has a README.md with build instructions and the
required system packages.