Skip to content

Adding a New Binding

Elbasiouny, Mahmoud edited this page May 28, 2026 · 1 revision

This page is a step-by-step guide for contributors who want to add a language binding to Hammer. It covers both the SWIG-based approach (reusing the shared interface file) and a handwritten approach (writing directly against the C API, as the C++ binding does).

Prerequisites: A working Hammer build. See Getting Started and DEVELOPMENT.md.


Two Approaches

Approach When to use Examples
SWIG The target language has a SWIG backend and you can express all necessary type conversions as SWIG typemaps Python, Java
Handwritten The target language has no SWIG backend, or the type system requires custom marshalling logic that SWIG can't express cleanly C++

For most languages, SWIG is the faster path. The shared interface file src/bindings/swig/hammer.i already covers all public Hammer combinators and handles several tricky type pairs; you only need to add language-specific typemaps.


Step 1 — Register the binding name in SConstruct

Open SConstruct and find the ListVariable near the top:

vars.Add(
    ListVariable("bindings", "Language bindings to build", "none", ["python", "java", "cpp"])
)

Add your language's name (lowercase, no spaces) to the list:

vars.Add(
    ListVariable("bindings", "Language bindings to build", "none", ["python", "java", "cpp", "ruby"])
)

This name becomes the bindings= flag value (scons bindings=ruby) and the directory name under src/bindings/.


Step 2 — Create the binding directory

Create src/bindings/<lang>/ with at minimum:

src/bindings/ruby/
    SConscript          # build, test, and install rules
    README.md           # user-facing API reference
    hammer_tests.rb     # (or equivalent) test suite

The SConscript is the only file required for the build; the rest are conventions shared by existing bindings.


Step 3 — Write the SConscript

Every binding SConscript follows the same skeleton. The sections below show the pattern; see src/bindings/python/SConscript and src/bindings/java/SConscript for full working examples.

3a. Boilerplate imports and guards

from __future__ import absolute_import, division, print_function
import os, shutil, sys

Import("env libhammer_shared testruns targets binding_results binding_test_stamps")

# Bindings require the shared library; static-only builds skip them.
if libhammer_shared is None:
    print("Warning: <Lang> bindings require the shared library. Skipping.")
    Return()

# Guard for missing toolchain dependencies.
if not shutil.which("swig"):          # or javac, rustc, etc.
    print("Warning: swig not found. Skipping <Lang> bindings.")
    Return()

The libhammer_shared guard is important: coverage and gprof builds produce only a static library and set libhammer_shared = None.

3b. Build target

Clone the environment so your changes don't affect other parts of the build:

langenv = env.Clone(IMPLICIT_COMMAND_DEPENDENCIES=0)

SWIG-based

Copy the interface file to the build directory, run SWIG, then compile the wrapper:

project_root = Dir("#").abspath
src_build_dir = os.path.join(project_root, env["BUILD_BASE"], "src")
hammer_lib_dir = os.path.dirname(str(libhammer_shared[0]))

swig_iface = langenv.Command(
    "hammer.i", "#src/bindings/swig/hammer.i", Copy("$TARGET", "$SOURCE")
)

# SWIG generates a C wrapper and language-side source files.
swig_out = langenv.Command(
    "hammer_wrap.c",
    swig_iface,
    "swig -<lang> -I" + src_build_dir + " -o $TARGET $SOURCE",
)

langenv.Append(CPPPATH=[src_build_dir])
langenv.Append(CCFLAGS=["-fPIC", "-DSWIG", "-std=gnu99", "-Wno-strict-aliasing"])
jni_lib = langenv.SharedLibrary(
    "hammer_<lang>",
    swig_out,
    LIBS=["hammer"],
    LIBPATH=[hammer_lib_dir],
)
Default(jni_lib)

Replace -<lang> with the SWIG language flag (e.g., -ruby, -go, -csharp).

Handwritten

Compile directly against libhammer headers:

src_dir = os.path.join(project_root, "src")
lang_src_dir = os.path.join(project_root, "src/bindings/<lang>")
hammer_lib_dir = os.path.dirname(str(libhammer_shared[0]))

langenv.Append(CPPPATH=[lang_src_dir, src_dir])
langenv.Append(LIBS=["hammer"])
langenv.Append(LIBPATH=[hammer_lib_dir])

lang_lib = langenv.SharedLibrary("hammer_<lang>", ["hammer_<lang>.c"])
Default(lang_lib)

3c. Test target

Run the test suite through tools/test_reporter.py, which writes a results file and feeds the summary printer in SConstruct:

reporter = os.path.join(Dir("#").abspath, "tools", "test_reporter.py")
langdir  = os.path.join(Dir("#").abspath, env["BUILD_BASE"], "src/bindings/<lang>")
results_file = os.path.join(langdir, "hammer_<lang>.results")

langtestexec = langenv.Command(
    "hammer_tests.stamp",
    ["hammer_tests.<ext>"] + list(lang_lib),
    [
        "LD_LIBRARY_PATH=%s %s %s --binding <Lang> --results-file %s -- <test-runner-command>"
        % (hammer_lib_dir, sys.executable, reporter, results_file),
        "touch $TARGET",
    ],
)
langtest = Alias("test<lang>", [langtestexec], langtestexec)
AlwaysBuild(langtestexec)
testruns.append(langtest)
binding_results.append(("<Lang>", results_file))
binding_test_stamps.append(langtestexec[0])

The binding_results and binding_test_stamps lists are what SConstruct reads to print the per-binding pass/fail summary at the end of scons test. Always append to both.

AlwaysBuild ensures scons test re-runs the tests even when the stamp file is up to date.

3d. Install target

langinstallexec = langenv.Command(
    None, lang_lib, "<install command>"
)
langinstall = Alias("install<lang>", [langinstallexec], langinstallexec)
targets.append(langinstall)

Use the language's native install tool when one exists (e.g., gem install, pip install, cargo install) for cross-platform compatibility.


Step 4 — Add language-specific typemaps to hammer.i (SWIG path only)

src/bindings/swig/hammer.i uses #if defined(SWIG<LANG>) guards to isolate per-language code. Add a new block for your language:

#if defined(SWIG<LANG>)

// Map byte[] ↔ (const uint8_t* input, size_t length)
%typemap(...) (const uint8_t* input, size_t length) { ... }
%apply (const uint8_t* input, size_t length) {
    (uint8_t* str, size_t len),
    (const uint8_t* str, const size_t len),
    (const uint8_t* charset, size_t length)
}

// uint8_t — map to the language's closest unsigned-byte type.
%typemap(in) uint8_t { ... }

// void*[] (NULL-terminated parser array)
%typemap(...) void*[] { ... }
%typemap(freearg) void*[] { free($1); }

#endif

The four conversions listed above are the minimum needed to expose the public API. Look at the Python and Java blocks in hammer.i for concrete examples of how each typemap is structured.

If the SWIG language flag is SWIG<LANG> (e.g., SWIGPYTHON, SWIGJAVA), check the SWIG documentation for the exact macro name for your target language.


Step 5 — Write a test suite

Each binding should include tests that cover:

  • Basic combinator round-trip: construct a parser, run parse() on known input, assert the result.
  • Sequence and choice combinators.
  • action / attr_bool callbacks (for languages that support passing functions as arguments).
  • A parse failure case (input that should not match).

Look at src/bindings/python/hammer_tests.py and src/bindings/java/HammerTests.java for the scope and style expected.


Step 6 — Update the wiki

After the binding is working:

  1. Add a README.md to src/bindings/<lang>/ following the structure of the Python or Java binding README.
  2. Add an entry to _Sidebar.md under "References":
    - [Adding a New Binding](Adding-a-New-Binding)
    (It is already there; add a link to your binding's README if you publish it separately.)
  3. Update DEVELOPMENT.md to list the new bindings=<lang> option and its test alias.

Checklist

  • Language name added to ListVariable in SConstruct
  • src/bindings/<lang>/SConscript created with build, test, and install sections
  • Import line includes all six shared names: env libhammer_shared testruns targets binding_results binding_test_stamps
  • libhammer_shared is None guard present
  • Test command appends to both binding_results and binding_test_stamps
  • AlwaysBuild called on the test target
  • (SWIG) Language-specific typemaps added to hammer.i under #if defined(SWIG<LANG>)
  • Test suite covers basic parse, failure, and combinators
  • src/bindings/<lang>/README.md written
  • DEVELOPMENT.md updated

See also: Extending Hammer · Hammer Fundamentals · Home

Clone this wiki locally