Scapegoat TIL

Building a .so sqlite extension in go

Building a SQLite Loadable Extension (.so/.dylib) from Go code

This guide documents how we added a SQLite loadable extension for REGEXP to go-sqlite-regexp, what pitfalls we hit, and a clean recipe to repeat next time.

The end result is a shared library you can load in the SQLite CLI or any embedding that supports extension loading:

Load and use in the sqlite3 shell:

.load ./regexp
SELECT 'hello' REGEXP 'h.llo';   -- 1
SELECT regexp('h.llo','hello');  -- 1

Overview

Files Added

Key C code

#include <sqlite3ext.h>
#include <stdlib.h>
#include "regexp_extension.h"
SQLITE_EXTENSION_INIT1

// Forward declarations for Go
extern void go_regexp(sqlite3_context *ctx, int argc, sqlite3_value **argv);
extern int go_register_regexp(sqlite3* db);

// Helper to index argv
sqlite3_value* value_at(sqlite3_value **argv, int idx) { return argv[idx]; }
const unsigned char* value_text(sqlite3_value* v) { return sqlite3_value_text(v); }
void result_null(sqlite3_context* ctx) { sqlite3_result_null(ctx); }
void result_error(sqlite3_context* ctx, const char* msg) { sqlite3_result_error(ctx, msg, -1); }
void result_int(sqlite3_context* ctx, int v) { sqlite3_result_int(ctx, v); }

// C-visible trampoline that calls into Go implementation
static void call_go_regexp(sqlite3_context *ctx, int argc, sqlite3_value **argv) {
    go_regexp(ctx, argc, argv);
}

// Helper to register the function with SQLite
int create_regexp(sqlite3* db) {
    return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, NULL, call_go_regexp, NULL, NULL);
}

int sqlite3_regexp_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    SQLITE_EXTENSION_INIT2(pApi);
    return go_register_regexp(db);
}

Notes:

Key Go code

package main

// #cgo pkg-config: sqlite3
// #include <stdlib.h>
// #include "regexp_extension.h"
import "C"

import (
    "regexp"
    "unsafe"
)

//export go_register_regexp
func go_register_regexp(db *C.sqlite3) C.int {
    rc := C.create_regexp(db)
    if rc != C.SQLITE_OK {
        return rc
    }
    return C.SQLITE_OK
}

//export go_regexp
func go_regexp(ctx *C.sqlite3_context, argc C.int, argv **C.sqlite3_value) {
    if argc != 2 {
        msg := C.CString("regexp(): requires exactly 2 arguments: pattern, text")
        C.result_error(ctx, msg)
        C.free(unsafe.Pointer(msg))
        return
    }

    vPattern := C.value_at(argv, 0)
    vText := C.value_at(argv, 1)

    cText := (*C.uchar)(C.value_text(vText))
    cPattern := (*C.uchar)(C.value_text(vPattern))

    if cText == nil || cPattern == nil {
        C.result_null(ctx)
        return
    }

    pattern := C.GoString((*C.char)(unsafe.Pointer(cPattern)))
    text := C.GoString((*C.char)(unsafe.Pointer(cText)))

    compiled, err := regexp.Compile(pattern)
    if err != nil {
        msg := C.CString(err.Error())
        C.result_error(ctx, msg)
        C.free(unsafe.Pointer(msg))
        return
    }

    if compiled.MatchString(text) {
        C.result_int(ctx, 1)
    } else {
        C.result_int(ctx, 0)
    }
}

Notes:

Makefile targets

# Build loadable SQLite extension (.so/.dylib) using c-shared
# Output goes to dist/regexp.$(EXT)
so: so-linux

so-linux:
	@echo "Building SQLite loadable extension for Linux (.so)..."
	@CGO_ENABLED=1 go build -buildmode=c-shared -o regexp.so ./extension

so-darwin:
	@echo "Building SQLite loadable extension for macOS (.dylib)..."
	@CGO_ENABLED=1 go build -buildmode=c-shared -o regexp.dylib ./extension

Build with:

Why split C and Go?

Common pitfalls we encountered

  1. Macro calls from Go
  1. SQLITE_EXTENSION_INIT2 in Go preamble
  1. Multiple definitions during linking
  1. Missing C standard headers
  1. Argument order confusion

End-to-end test

Non-interactive CLI test on Linux:

sqlite3 -batch ":memory:" -cmd ".load ./regexp" \
  "SELECT regexp('h.llo','hello');" \
  "SELECT regexp('^h.*o$','hello');" \
  "SELECT regexp('d','abc');" \
  "SELECT 'hello' REGEXP 'h.llo';" \
  "SELECT 'hello' REGEXP '^h.*o$';" \
  "SELECT 'abc' REGEXP 'd';"
# Expected output:
# 1
# 1
# 0
# 1
# 1
# 0

Clean recipe to repeat next time

  1. Create extension/regexp_extension.c with:
  1. Create extension/regexp_extension.h declaring the C wrappers used from Go.

  2. Create extension/regexp_extension.go:

  1. Add extension/main_dummy.go with an empty main() to satisfy -buildmode=c-shared.

  2. Add Make targets:

  1. Test in the sqlite3 CLI with .load ./regexp and sample queries.