Gerbil Scheme FFI Example Using the C Language Raptor RDF Library
The Foreign Function Interface (FFI) in Gerbil Scheme provides a powerful bridge to leverage existing C libraries directly within Scheme programs. This allows developers to extend Scheme applications with highly optimized, domain-specific functionality written in C, while still enjoying the high-level abstractions and rapid development style of Scheme. In this chapter, we will explore an end-to-end example of integrating the Raptor RDF parsing and serialization library into Gerbil Scheme, showing how to bind C functions and expose them as Scheme procedures.
Raptor is a mature C library for parsing, serializing, and manipulating RDF (Resource Description Framework) data in a variety of syntaxes, including RDF/XML, Turtle, N-Triples, and JSON-LD. By accessing Raptor from Gerbil Scheme, we open the door to semantic web applications, linked data processing, and graph-based reasoning directly within a Scheme environment. This example illustrates the mechanics of building FFI bindings, handling C-level memory and data types, and translating them into idiomatic Scheme representations. For reference, the official Raptor API documentation is available here: Raptor2 API Reference.
We will walk through the process step by step: setting up the Gerbil Scheme FFI definitions, mapping C functions and structs into Scheme, and writing test programs that parse RDF data and extract triples. Along the way, we will highlight practical issues such as error handling, symbol exporting, and resource cleanup. By the end of this chapter, you should have both a working Gerbil Scheme binding to Raptor and a general blueprint for integrating other C libraries into your Scheme projects. For background on Gerbil Scheme’s FFI itself, consult the Gerbil Documentation: FFI.
Implementation of a FFI Bridge Library for Raptor
The library is located in the file gerbil_scheme_book/source_code/RaptorRDF_FFI/ffi.ss. This code demonstrates how to use the Foreign Function Interface (FFI) to integrate with the Raptor RDF C library. It provides a Scheme-accessible procedure, raptor-parse-file->ntriples, which parses an RDF file in a specified syntax (such as Turtle or RDF/XML) and returns the results as an N-Triples–formatted string. This example highlights the practical use of FFI in Gerbil Scheme: exposing a C function to Scheme, managing memory safely across the boundary, and translating RDF data into a representation that Scheme programs can process directly.
1 (export raptor-parse-file->ntriples)
2
3 (import :std/foreign)
4
5 (begin-ffi (raptor-parse-file->ntriples)
6 (c-declare #<<'C'
7 #include <raptor2.h>
8 #include <string.h>
9
10 #ifndef RAPTOR_STRING_ESCAPE_FLAG_NTRIPLES
11 #define RAPTOR_STRING_ESCAPE_FLAG_NTRIPLES 0x4
12 #endif
13
14 /* Write one triple to the iostream in N-Triples and a newline */
15 static void triples_to_iostr(void* user_data, raptor_statement* st) {
16 raptor_iostream* iostr = (raptor_iostream*)user_data;
17
18 raptor_term_escaped_write(st->subject, RAPTOR_STRING_ESCAPE_FLAG_NTRIPLES, iostr);
19 raptor_iostream_write_byte(' ', iostr);
20 raptor_term_escaped_write(st->predicate, RAPTOR_STRING_ESCAPE_FLAG_NTRIPLES, iostr);
21 raptor_iostream_write_byte(' ', iostr);
22 raptor_term_escaped_write(st->object, RAPTOR_STRING_ESCAPE_FLAG_NTRIPLES, iostr);
23 raptor_iostream_write_byte(' ', iostr);
24 raptor_iostream_write_byte('.', iostr);
25 raptor_iostream_write_byte('\n', iostr);
26 }
27
28 /* Parse `filename` with syntax `syntax_name` and return N-Triples as char*.
29 The returned memory is owned by Raptor's allocator; Gambit copies it
30 into a Scheme string via char-string return convention. */
31 static char* parse_file_to_ntriples(const char* filename, const char* syntax_name) {
32 raptor_world *world = NULL;
33 raptor_parser* parser = NULL;
34 unsigned char *uri_str = NULL;
35 raptor_uri *uri = NULL, *base_uri = NULL;
36 raptor_iostream *iostr = NULL;
37 void *out_string = NULL;
38 size_t out_len = 0;
39
40 world = raptor_new_world();
41 if(!world) return NULL;
42 if(raptor_world_open(world)) { raptor_free_world(world); return NULL; }
43
44 /* Where triples go: a string iostream that materializes on free */
45 iostr = raptor_new_iostream_to_string(world, &out_string, &out_len, NULL);
46 if(!iostr) { raptor_free_world(world); return NULL; }
47
48
49
50 parser = raptor_new_parser(world, syntax_name ? syntax_name : "guess");
51 if(!parser) { raptor_free_iostream(iostr); raptor_free_world(world); return NULL; }
52
53 raptor_parser_set_statement_handler(parser, iostr, triples_to_iostr);
54
55 uri_str = raptor_uri_filename_to_uri_string((const unsigned char*)filename);
56 if(!uri_str) { raptor_free_parser(parser); raptor_free_iostream(iostr); raptor_free_world(world); return NULL; }
57
58 uri = raptor_new_uri(world, uri_str);
59 base_uri = raptor_uri_copy(uri);
60
61 /* Parse file; on each triple our handler appends to iostr */
62 raptor_parser_parse_file(parser, uri, base_uri);
63
64 /* Clean up parser/URIs; free iostr LAST to finalize string */
65 raptor_free_parser(parser);
66 raptor_free_uri(base_uri);
67 raptor_free_uri(uri);
68 raptor_free_memory(uri_str);
69
70 raptor_free_iostream(iostr); /* this finalizes out_string/out_len */
71
72 /* Keep world only as long as needed; string is independent now */
73 raptor_free_world(world);
74
75 return (char*)out_string; /* Gambit copies to Scheme string */
76 }
77 'C'
78 )
79
80 ;; Scheme visible wrapper:
81 (define-c-lambda raptor-parse-file->ntriples
82 (char-string ;; filename
83 char-string) ;; syntax name, e.g., "turtle", "rdfxml", or "guess"
84 char-string
85 "parse_file_to_ntriples"))
The C portion begins by including the raptor2.h header and defining a callback function, triples_to_iostr, which takes RDF statements and writes them to a Raptor iostream in N-Triples format. This callback escapes subjects, predicates, and objects correctly and ensures triples are terminated with a period and newline, conforming to the N-Triples standard. The main work is performed in parse_file_to_ntriples, which initializes a Raptor world and parser, configures the statement handler to use the callback, and sets up an iostream that accumulates parsed triples into a string buffer. Error checks are in place at every step, ensuring resources such as the world, parser, URIs, and iostream are properly freed if initialization fails.
After setup, the parser processes the input file identified by its filename and syntax. Each RDF statement is converted into N-Triples and appended to the output string via the iostream. Once parsing is complete, the parser, URIs, iostream, and world are released, leaving a fully materialized string containing the N-Triples serialization. This string is returned to Scheme through the FFI, where Gambit copies it into a managed Scheme string. On the Scheme side, the define-c-lambda form binds this C function as the procedure raptor-parse-file->ntriples, exposing it with the expected (filename syntax-name) -> ntriples-string interface. The result is a clean abstraction: Scheme code can call raptor-parse-file->ntriples with an RDF file and syntax, receiving back normalized N-Triples ready for further processing in Gerbil Scheme.
Test Code
This Gerbil Scheme test code in the file test.ss exercises the FFI binding raptor-parse-file->ntriples by creating a minimal Turtle input, invoking the parser in two modes (“turtle” and “guess”), and asserting that both produce the same canonical N-Triples output. It’s designed to be self-contained: it writes a temporary .ttl file, runs the conversion twice, compares results against an expected string, then cleans up and prints status.
1 ;; Simple test for ffi.ss: validates N-Triples output
2
3 (import "ffi")
4 (export main)
5 (import "ffi" :gerbil/gambit)
6
7 (define (write-file path content)
8 (let ((p (open-output-file path)))
9 (display content p)
10 (close-output-port p)))
11
12 (define (read-file path)
13 (let ((p (open-input-file path)))
14 (let loop ((chunks '()))
15 (let ((c (read-char p)))
16 (if (eof-object? c)
17 (begin (close-input-port p)
18 (list->string (reverse chunks)))
19 (loop (cons c chunks)))))))
20
21 (define (assert-equal expected actual label)
22 (if (equal? expected actual)
23 (begin (display "PASS ") (display label) (newline) #t)
24 (begin
25 (display "FAIL ") (display label) (newline)
26 (display "Expected:\n") (display expected) (newline)
27 (display "Actual:\n") (display actual) (newline)
28 (exit 1))))
29
30 (define (main . args)
31 (let* ((ttl-file "sample.ttl")
32 (ttl-content "@prefix ex: <http://example.org/> .
33 ex:s ex:p ex:o .
34 ")
35 (expected-nt "<http://example.org/s> <http://example.org/p> <http://example.org/o> .
36 "))
37 ;; Prepare sample Turtle file
38 (write-file ttl-file ttl-content)
39
40 ;; Exercise FFI with explicit syntax
41 (let ((nt1 (raptor-parse-file->ntriples ttl-file "turtle")))
42 (assert-equal expected-nt nt1 "turtle -> ntriples"))
43
44 ;; Exercise FFI with syntax guessing
45 (let ((nt2 (raptor-parse-file->ntriples ttl-file "guess")))
46 (assert-equal expected-nt nt2 "guess -> ntriples"))
47
48 ;; Clean up
49 (when (file-exists? ttl-file)
50 (delete-file ttl-file))
51
52 (display "All tests passed.
53 ")))
This code exports main and imports the FFI wrapper. Utility helpers include write-file (persist a string to disk), read-file (characterwise file read; defined but unused here), and assert-equal, which prints PASS/FAIL labels and exits with non-zero status on mismatch. In function main a small Turtle document defines a simple triple using the ex: prefix; the corresponding expected N-Triples string is the fully expanded IRI form with a terminating period and newline.
The test proceeds in two phases: first it calls raptor-parse-file->ntriples ttl-file “turtle” and checks the result; then it repeats using “guess” to confirm the parser’s auto-detection path yields identical serialization. After both assertions pass, it deletes the temporary file and prints “All tests passed.” The result is a minimal but effective smoke test verifying the FFI, Raptor’s parsing/serialization, and the contract that both explicit syntax selection and guessing produce stable N-Triples output.
We use a Makefile to build an executable on macOS:
1 ##### macOS:
2 RAPTOR_PREFIX ?= /opt/homebrew/Cellar/raptor/2.0.16
3 OPENSSL_PREFIX ?= /opt/homebrew/opt/openssl@3
4 CC_OPTS := -I$(RAPTOR_PREFIX)/include/raptor2
5 LD_OPTS := -L$(RAPTOR_PREFIX)/lib -lraptor2 -L$(OPENSSL_PREFIX)/lib
6
7 build:
8 gxc -cc-options "$(CC_OPTS)" -ld-options "$(LD_OPTS)" ffi.ss
9 gxc -cc-options "$(CC_OPTS)" -ld-options "$(LD_OPTS)" -exe -o test test.ss
10
11 clean:
12 rm -f *.c *.scm *.o *.so test
Alternatively, the Makefile for Linux can me run using make -f Makefile.linux:
1 #### Ubuntu Linux
2 CC_OPTS += $(shell pkg-config --cflags raptor2)
3 LD_OPTS += $(shell pkg-config --libs raptor2)
4
5 build:
6 gxc -cc-options "$(CC_OPTS)" -ld-options "$(LD_OPTS)" ffi.ss
7 gxc -cc-options "$(CC_OPTS)" -ld-options "$(LD_OPTS)" -exe -o test test.ss
8
9 clean:
10 rm -f *.c *.scm *.o *.so test
Test output is:
1 $ ./test
2 PASS turtle -> ntriples
3 PASS guess -> ntriples
4 All tests passed.