Sheet K3 · Reference

C kit

Building and testing a C plugin, and every function, type and macro in the headers.

The C kit is a set of headers in include/, built on Extism's C PDK (extism-pdk.h). It also has two scripts: build.sh builds a plugin into a .hammerplugin, and test.sh runs its native unit tests. Your plugin is a single C file, compiled to freestanding wasm32 without libc. Hammer's own C plugins (plaintext, simple-grammar, and style, in hammer-plugins) are fuller examples.

A complete plugin

This plugin is a text check that suggests cutting "very". It lives in a directory named very/, which contains manifest.toml and very.c.

id = "very"
name = "Very"
version = "0.1.0"
api = 1
description = "Underlines “very”, which rarely helps."

[[diagnostics]]
name = "very"
label = "Very"
#define HAMMER_IMPLEMENTATION
#include "hammer.h"
#include "hammer_diagnose.h"

int hammer_diagnose_begin(const HammerDiagnoseRequest *request) {
  (void)request;
  return 1;
}

void hammer_diagnose_paragraph(HammerText text) {
  uint64_t count;
  HammerWord *words = hammer_words(text, &count);
  for (uint64_t i = 0; i < count; i++) {
    if (!hammer_word_is(text, words[i], "very")) continue;
    uint64_t end = words[i].end;
    if (end < text.length && text.data[end] == ' ') end++;
    HammerFix fix = {"", 0, "Remove “very”"};
    hammer_suggest(words[i].start, end, "“Very” rarely helps", &fix, 1);
  }
}
hammer-plugin-sdk-c-0.8.0/build.sh very

This writes very/build/very.wasm and very/build/very.hammerplugin. To install the package, open Settings, Plugins in Hammer. The manifest page describes the manifest's keys.

Setup

The quickest start is the C template: a working plugin with tests and a release workflow, with the kit's headers vendored in include/ and its own build.sh and test.sh. Choose Use this template on its GitHub page to make your own repository from it.

C has no package registry, so the kit ships as an asset of each hammer-plugin-sdk release: hammer-plugin-sdk-c-<version>.tar.gz, with its SHA-256 checksum beside it as hammer-plugin-sdk-c-<version>.tar.gz.sha256. The tarball holds one directory, hammer-plugin-sdk-c-<version>/, containing include/, build.sh, test.sh, package.sh, and the SDK's license; Extism's license is in include/.

curl -sSfLO https://github.com/Darkrock-Studios/hammer-plugin-sdk/releases/download/v0.8.0/hammer-plugin-sdk-c-0.8.0.tar.gz
curl -sSfLO https://github.com/Darkrock-Studios/hammer-plugin-sdk/releases/download/v0.8.0/hammer-plugin-sdk-c-0.8.0.tar.gz.sha256
sha256sum -c hammer-plugin-sdk-c-0.8.0.tar.gz.sha256
tar xzf hammer-plugin-sdk-c-0.8.0.tar.gz

Unpack it anywhere and pass your plugin directories to its scripts, or vendor include/ into your repository, as the template does. HAMMER_KIT_VERSION in hammer.h says which release a copy of the headers is from. In a checkout of hammer-plugin-sdk, the scripts are c/build.sh and c/test.sh, the headers are in c/include/, and package.sh is tools/package.sh.

To build, you need clang with the wasm32 target, wasm-ld (from lld), and zip. On Debian or Ubuntu: sudo apt install clang lld zip.

One file defines the implementation

The headers declare everything, but they define the kit's functions only where HAMMER_IMPLEMENTATION is defined. Define it in exactly one source file, before the first include. build.sh compiles a single file, <dir>/<dir>.c, so that's normally your plugin's only .c file.

#define HAMMER_IMPLEMENTATION
#define HAMMER_OUT_OF_MEMORY "The text is too long to check"  // optional
#include "hammer.h"
#include "hammer_diagnose.h"

HAMMER_IMPLEMENTATION also defines EXTISM_IMPLEMENTATION, so don't define that yourself. It applies to every kit header you include after it: hammer_out.h and hammer_diagnose.h define their functions in that file too, and hammer_diagnose.h also defines the diagnose export. The functions in the other headers are static inline, apart from hammer_test.h's, which are static.

Include hammer.h first. hammer_out.h, hammer_text.h, hammer_archive.h, hammer_action.h, and hammer_diagnose.h include it themselves. hammer_text.h includes hammer_word_classes.h, and hammer_image.h includes hammer_text.h. hammer_archive.h includes hammer_base64.h and hammer_out.h. hammer_action.h and hammer_diagnose.h include hammer_json.h, hammer_out.h, and hammer_text.h. hammer_json.h and hammer_base64.h need nothing but <stdint.h>.

Memory, once per call

The kit has no malloc. Instead, hammer_alloc hands out scratch memory from the top of the module's linear memory, and it never frees that memory on its own. At the start of each export you write, call hammer_alloc_reset(), then read the input into fresh memory. The reset hands out again any memory that pointers from an earlier call pointed at, so after the reset, zero any static HammerBuffer and clear any static pointer. To keep something across calls, such as a loaded resource, call hammer_alloc_keep(): it keeps everything you've allocated so far.

Building

build.sh <plugin directory>...

For each directory you name, build.sh compiles <dir>/<dir>.c to <dir>/build/<dir>.wasm, where <dir> is the directory's own name. If the directory has a manifest.toml, it then runs package.sh to pack <dir>/build/<dir>.hammerplugin: the one beside it in the tarball, or tools/package.sh in a checkout of hammer-plugin-sdk. It also prints the module's size.

The compile targets freestanding wasm32, with no libc or WASI:

clang --target=wasm32-unknown-unknown -O2 -nostdlib -I "$kit/include" \
    -Wl,--no-entry -Wl,--export-dynamic \
    -o "$dir/build/$name.wasm" "$dir/$name.c"
Flag Why
--target=wasm32-unknown-unknown Builds a bare WebAssembly module, which is what Extism loads
-nostdlib Leaves out libc. hammer.h supplies memset, memcpy, and strlen, which clang emits calls to; nothing else from libc is available
-I "$kit/include" Finds the kit's headers, including extism-pdk.h
-Wl,--no-entry A module has no main
-Wl,--export-dynamic Keeps the exports marked with EXTISM_EXPORTED_FUNCTION

package.sh

package.sh <plugin directory> <module.wasm>

Packs <dir>/build/<dir>.hammerplugin. The package is a zip containing manifest.toml, the module (as plugin.wasm), and settings.toml, locales/, and resources/ if the directory has them. build.sh runs it for you.

Testing

test.sh <plugin directory>...

For each directory, test.sh compiles <dir>/test.c for the machine it's running on, with HAMMER_NATIVE defined, and then runs it with HAMMER_RESOURCES set to <dir>/resources. The compile command is:

clang -DHAMMER_NATIVE -std=c11 -g -O1 -Wall -Wextra -Werror -Wno-unused-function -I "$kit/include" \
    -fsanitize=address,undefined -fno-sanitize-recover=all \
    -o "$dir/build/test" "$dir/test.c"

Warnings are errors, and the address and undefined behaviour sanitizers stop at the first fault. The -Wno-unused-function flag is there because a test includes the plugin's whole source, so functions that only the exports call go unused.

A test.c file includes your plugin's source, then hammer_test.h:

#include "simple-grammar.c"
#include "hammer_test.h"

int main(void) {
  HAMMER_EXPECT(0, "I saw the the cat.", "[the the]>the");
  return hammer_test_summary();
}

With HAMMER_NATIVE defined, the kit uses libc and leaves Extism out. Memory, hammer_fail, resources, and everything built on them work. Operations (hammer_call) and the cache don't exist. If your plugin defines an export itself with EXTISM_EXPORTED_FUNCTION, put that export inside #ifndef HAMMER_NATIVE so the test still compiles; hammer_diagnose.h does this for its own export. Put the logic you want to test in functions that the export calls.

Exports

A plugin has at most one export of each kind: export, action, diagnose, and command. If your plugin has two actions, its one action export tells them apart by name. The pages on export formats, actions, text checks and commands describe the requests and replies.

Export How you define it in C
export int32_t EXTISM_EXPORTED_FUNCTION(export), reading the request with hammer_json.h, and writing an archive's reply with hammer_archive.h
action int32_t EXTISM_EXPORTED_FUNCTION(action), using hammer_action.h
diagnose Define hammer_diagnose_begin and hammer_diagnose_paragraph; hammer_diagnose.h defines the export
command int32_t EXTISM_EXPORTED_FUNCTION(command), reading the request with hammer_json.h

An export returns 0 on success. To fail, set an error and return 1, or call hammer_fail, which does both and doesn't return.

export

The export export receives the story's chapters and returns the file's bytes. For a format with input = "prose", each scene arrives as blocks in prose instead of as markdown in scenes. A format with output = "archive" returns a list of files instead, which Hammer zips; write it with hammer_archive.h. A format with images = true also gets the project images the story refers to, in images, with their bytes as base64; find the references with hammer_image.h and decode the bytes with hammer_base64.h.

#define HAMMER_IMPLEMENTATION
#include "hammer.h"
#include "hammer_json.h"
#include "hammer_out.h"

int32_t EXTISM_EXPORTED_FUNCTION(export) {
  hammer_alloc_reset();
  uint64_t size = extism_input_length();
  char *input = hammer_must_alloc(size);
  extism_load_input(0, input, size);

  HammerBuffer out = {0};
  HammerJson json;
  if (hammer_json_find(input, size, "projectName", &json)) {
    const char *raw;
    uint64_t raw_length;
    if (hammer_json_string(&json, &raw, &raw_length)) {
      hammer_reserve(&out, raw_length);
      out.length += hammer_json_unescape(raw, raw_length, out.data + out.length);
    }
  }
  hammer_put_char(&out, '\n');
  extism_output_buf(out.data, out.length);
  return 0;
}

action

The action export runs an action that the manifest declares under [[actions]]. See actions for pages, fields, buttons, and state.

#define HAMMER_IMPLEMENTATION
#include "hammer.h"
#include "hammer_action.h"

int32_t EXTISM_EXPORTED_FUNCTION(action) {
  hammer_alloc_reset();
  uint64_t size = extism_input_length();
  char *input = hammer_must_alloc(size);
  extism_load_input(0, input, size);

  HammerActionRequest request;
  if (!hammer_action_read(input, size, &request)) hammer_fail("Hammer sent a request this could not read");

  HammerBuffer out = {0};
  if (!request.button.data) {
    HammerField fields[] = {{.type = HAMMER_FIELD_TEXT, .key = "name", .label = "Name", .required = 1}};
    HammerButton buttons[] = {{.id = "greet", .label = "Greet", .primary = 1}};
    HammerPage page = {.title = "Greet", .fields = fields, .field_count = 1, .buttons = buttons, .button_count = 1};
    hammer_put_page(&out, &page, 0);
  } else {
    hammer_put_message(&out, "Hello!");
  }
  extism_output_buf(out.data, out.length);
  return 0;
}

diagnose

Include hammer_diagnose.h in the file that defines HAMMER_IMPLEMENTATION, then define hammer_diagnose_begin and hammer_diagnose_paragraph. The kit's export resets memory, reads the request, calls your two functions, and writes the reply. See hammer_diagnose.h.

command

The command export handles one line of standard input for a command declared under [[commands]]. Any output is written as one line of standard output. The kit has no command helper, so read the request with hammer_json.h.

#define HAMMER_IMPLEMENTATION
#include "hammer.h"
#include "hammer_json.h"
#include "hammer_out.h"

int32_t EXTISM_EXPORTED_FUNCTION(command) {
  hammer_alloc_reset();
  uint64_t size = extism_input_length();
  char *input = hammer_must_alloc(size);
  extism_load_input(0, input, size);

  HammerBuffer out = {0};
  HammerJson json;
  const char *raw;
  uint64_t raw_length;
  if (hammer_json_find(input, size, "line", &json) && hammer_json_string(&json, &raw, &raw_length)) {
    hammer_put_text(&out, "Read ");
    hammer_put_int(&out, (int64_t)raw_length);
    hammer_put_text(&out, " bytes");
  }
  extism_output_buf(out.data, out.length);
  return 0;
}

extism-pdk.h

The kit is built on Extism's C PDK. This section covers the parts of it that a Hammer plugin calls; the rest belongs to Extism and isn't covered here.

ExtismHandle

typedef uint64_t ExtismHandle;

A handle to a block of Extism memory, outside the module's own memory. hammer_call, hammer_cache_read, and the raw imports return one. A handle of 0 means there's no block.

EXTISM_EXPORTED_FUNCTION

#define EXTISM_EXPORTED_FUNCTION(name)                                         \
  EXTISM_EXPORT_AS(#name)                                                      \
  name(void)

Declares an export that Hammer calls by name, as in int32_t EXTISM_EXPORTED_FUNCTION(action) { ... }. It isn't defined in a HAMMER_NATIVE build.

extism_input_length, extism_load_input

extern uint64_t extism_input_length(void);
static inline bool extism_load_input(const uint64_t src_offset, void *dest,
                                     const size_t n);

extism_input_length returns the request's length in bytes, and extism_load_input copies n of its bytes, starting at src_offset, into dest. Call hammer_alloc_reset() first, then read the whole request into memory from hammer_must_alloc.

extism_output_buf

void extism_output_buf(const void *src, const size_t n);

Sets the export's output to n bytes from src.

extism_error_set_buf_from_sz

void extism_error_set_buf_from_sz(const char *message);

Sets the call's error message. Return 1 from the export afterwards. hammer_fail does both for you.

extism_length, extism_load_from_handle, extism_free

extern uint64_t extism_length(const ExtismHandle);
bool extism_load_from_handle(const ExtismHandle src, const uint64_t src_offset,
                             void *dest, const size_t n);
extern void extism_free(ExtismHandle);

extism_length returns a handle's length, extism_load_from_handle copies its bytes into the module's memory, and extism_free frees it. Here's how to read a reply from hammer_call or hammer_cache_read:

ExtismHandle handle = hammer_call("scene.read", "{\"project\":\"My Novel\",\"id\":12}");
uint64_t size = extism_length(handle);
char *reply = hammer_must_alloc(size);
extism_load_from_handle(handle, 0, reply, size);
extism_free(handle);

hammer.h

The base of the kit. It covers the API and kit versions, memory, failing, progress, resources, operations, and the cache.

HAMMER_IMPLEMENTATION, HAMMER_NATIVE, EXTISM_USE_LIBC

#define HAMMER_IMPLEMENTATION
#define HAMMER_NATIVE
#define EXTISM_USE_LIBC

You define these yourself, before including hammer.h. Define HAMMER_IMPLEMENTATION in exactly one file, and the kit's functions are defined in that file. HAMMER_NATIVE builds for the machine doing the compiling, with libc and without Extism; test.sh sets it. If EXTISM_USE_LIBC isn't defined, the implementation also defines memset, memcpy, and strlen, which clang calls even from freestanding code.

HAMMER_API_VERSION

#define HAMMER_API_VERSION 1

The host API version these headers target. The manifest's api must match it.

HAMMER_KIT_VERSION

#define HAMMER_KIT_VERSION "0.8.0"

The hammer-plugin-sdk release these headers are from, for a plugin that keeps a copy of them.

HAMMER_OUT_OF_MEMORY

#ifndef HAMMER_OUT_OF_MEMORY
#define HAMMER_OUT_OF_MEMORY "The plugin ran out of memory"
#endif

The error that hammer_must_alloc reports. To word it for your plugin, define it before including hammer.h, as in #define HAMMER_OUT_OF_MEMORY "The story is too long for a style report".

hammer_alloc, hammer_alloc_reset

void *hammer_alloc(uint64_t size);
void hammer_alloc_reset(void);

hammer_alloc hands out scratch memory from the top of the module's linear memory. It grows the memory as needed and aligns each block to 8 bytes. Nothing is freed automatically: call hammer_alloc_reset() at the start of each export to reuse the memory. hammer_alloc returns 0 once the host's memory limit is reached. A reset goes back to the last hammer_alloc_keep, or to the start if there wasn't one. In a native build, the memory is a single 64 MiB block (HAMMER_NATIVE_MEMORY), which is as much as a plugin gets by default.

hammer_alloc_keep

void hammer_alloc_keep(void);

Keeps everything hammer_alloc has handed out so far for later calls, so resets only go back as far as this point. Kept memory is never reused, so keep only what later calls need. Call it early in a call, before scratch allocations pile up beneath it.

hammer_must_alloc

void *hammer_must_alloc(uint64_t size);

Calls hammer_alloc, and if memory runs out, calls hammer_fail with HAMMER_OUT_OF_MEMORY. The kit's own allocations (buffers, word lists, resources, and decoded paragraphs) all use it.

hammer_fail

__attribute__((noreturn)) void hammer_fail(const char *message);

Ends the call with message as its error, and doesn't return. In a native build, it prints hammer_fail: <message> to standard error and aborts.

hammer_progress

void hammer_progress(double fraction, const char *message);

Reports how far an action has got. fraction runs from 0 to 1, or is negative when you don't know. message is a line saying what the action is doing, or 0. Hammer shows the report while the action runs. If the writer has stopped the run, the call doesn't return, and the action ends there.

Outside an action, or in a native build, it does nothing. The report is built on the stack, so it uses none of hammer_alloc's memory. The fraction is sent only to the thousandth, and a long message is cut short.

for (uint64_t i = 0; i < scene_count; i++) {
  hammer_progress((double)i / (double)scene_count, "Reading scenes");
  ...
}

hammer_resource_load

const char *hammer_resource_load(const char *name, uint64_t *length);

Reads a file from the package's resources/ directory by its path there, such as "words/en.txt". The file is loaded into hammer_alloc's memory with a NUL after it, and *length is set to its length. Returns 0 if the package has no file by that name, or if the file is empty.

Hammer reads the file from the package on every call, so if you use a resource on every call, consider keeping it with hammer_alloc_keep. In a native build, it reads from the directory named in the HAMMER_RESOURCES environment variable, which test.sh sets to the plugin's resources/.

hammer_resource_lines

const char **hammer_resource_lines(const char *name, uint64_t *count);

Reads a resource that's a list, such as a word list, with hammer_resource_load, and returns its lines as a list ending with 0. Each line is trimmed of spaces, tabs, and a \r, and blank lines and # comments are left out. *count is set to the number of lines. Returns 0 if the package has no such file, or if it's empty. The file is split into lines in the memory it was loaded into, so the lines and the list are in hammer_alloc's memory for this call.

uint64_t count;
const char **words = hammer_resource_lines("en/filter-words.txt", &count);
if (words && hammer_word_in(text, word, words)) ...

hammer_call

ExtismHandle hammer_call(const char *operation, const char *input_json);

Runs a Hammer operation that the manifest has permission for. input_json is the operation's input object, such as {"project":"My Novel","id":12}. It's copied exactly as given, so it must be valid JSON. Returns a handle to the reply, which is either {"output": {...}} or {"error": {"kind": "NotFound" | "InvalidInput" | "PermissionDenied" | "Failed", "message": "..."}}. Load the reply with extism_load_from_handle and free it with extism_free. It isn't available in a native build.

The operations page lists each operation with its input and output (for example, scene.read), and the host page covers the host side.

HAMMER_CACHE_MAX_KEY, HAMMER_CACHE_MAX_VALUE

#define HAMMER_CACHE_MAX_KEY 1024
#define HAMMER_CACHE_MAX_VALUE (1024 * 1024)

The largest cache key and value, in bytes. The cache belongs to your plugin, and Hammer keeps it between calls and runs. Hammer may drop any value at any time: it keeps the most recently written values, up to a total size, and clears them all whenever the plugin is installed again or uninstalled. Nothing in the cache is synced or backed up. A call still running on the old version during an install may write after the cache is cleared, so put a version in any key whose value's format might change.

hammer_cache_read

ExtismHandle hammer_cache_read(const void *key, uint64_t key_length);

Returns a handle to the value cached under key, or 0 if there isn't one (or the key is over its size limit). Free the handle with extism_free. It isn't available in a native build.

hammer_cache_write

int hammer_cache_write(const void *key, uint64_t key_length, const void *value, uint64_t length);

Caches length bytes of value under key. An empty value removes the key. If the key or the value is over its limit, it stores nothing and returns 0; otherwise it returns 1. It isn't available in a native build.

Raw imports

EXTISM_IMPORT_USER("hammer_progress")
extern void hammer_progress_report(ExtismHandle report);

EXTISM_IMPORT_USER("hammer_resource")
extern ExtismHandle hammer_resource(ExtismHandle name);

EXTISM_IMPORT_USER("hammer_dispatch")
extern ExtismHandle hammer_dispatch(ExtismHandle request);

EXTISM_IMPORT_USER("hammer_cache_get")
extern ExtismHandle hammer_cache_get(ExtismHandle key);

EXTISM_IMPORT_USER("hammer_cache_set")
extern void hammer_cache_set(ExtismHandle key, ExtismHandle value);

These are Hammer's host functions, which the functions above wrap. Use the wrappers instead.

  • hammer_progress_report takes a handle to {"fraction": 0.5, "message": "..."}. Both keys are optional.
  • hammer_resource returns a handle to the resource named by the handle name, or 0.
  • hammer_dispatch takes a handle to {"operation": "<name>", "input": {...}}.
  • hammer_cache_get returns the key's value, or 0.
  • hammer_cache_set stores a value, or removes the key when the value is 0. A key or value over its limit ends the call with an error.

None of them exist in a native build. The host page describes them.

hammer_json.h

A small JSON reader that walks a document in place, without allocating. Strings come back as their raw contents, escapes included, as pointers into the document.

HammerJson json;
hammer_json_init(&json, text, length);
const char *key; uint64_t key_length;
if (hammer_json_enter_object(&json)) {
  while (hammer_json_next_key(&json, &key, &key_length)) {
    if (hammer_json_key_is(key, key_length, "name")) { ...read the value... }
    else hammer_json_skip(&json);
  }
}

You must read or skip every value before moving on. A malformed document sets error, and every later call returns nothing, so you can check error once at the end. To read ahead or go back, copy a HammerJson and set its at; plaintext does this to read chapters after settings.

HammerJson

typedef struct {
  const char *text;
  uint64_t length;
  uint64_t at;
  int error;
} HammerJson;

A reader over length bytes of text. at is the byte offset of the next thing to read. error is set as soon as the reader finds that the document is malformed.

hammer_json_init

static inline void hammer_json_init(HammerJson *json, const char *text, uint64_t length);

Starts reading text from its first byte. The text doesn't need to end with a NUL.

hammer_json_peek

static inline char hammer_json_peek(HammerJson *json);

Returns the first character of the next value, after any whitespace: {, [, ", t, f, n, or a digit or -. It returns 0 at the end of the document or after an error. It moves past whitespace only, so you can use it to learn a value's type before you read it.

hammer_json_enter_object, hammer_json_enter_array

static inline int hammer_json_enter_object(HammerJson *json);
static inline int hammer_json_enter_array(HammerJson *json);

Moves inside the object or array at the reader's position. If the next value isn't an object (or an array), it returns 0 and sets error.

hammer_json_next_key

static inline int hammer_json_next_key(HammerJson *json, const char **key, uint64_t *key_length);

Moves to the object's next key and reads it into *key (raw, as hammer_json_string gives it). It then moves past the colon, so the reader is at the key's value. When there are no more keys, it moves past the closing brace and returns 0.

hammer_json_next_item

static inline int hammer_json_next_item(HammerJson *json);

Moves to the array's next item. When there are no more items, it moves past the closing bracket and returns 0.

hammer_json_string

static inline int hammer_json_string(HammerJson *json, const char **raw, uint64_t *raw_length);

Reads a string and gives you its contents as they appear in the document, escapes included, as a pointer into the document. If the next value isn't a string, it returns 0 and sets error. Decode the contents with hammer_json_unescape.

hammer_json_bool

static inline int hammer_json_bool(HammerJson *json);

Reads a value and returns 1 if it's true, or 0 for anything else.

hammer_json_int

static inline int64_t hammer_json_int(HammerJson *json);

Reads a number as an integer. Any fraction or exponent is skipped, so 2.9 reads as 2.

hammer_json_skip

static inline void hammer_json_skip(HammerJson *json);

Moves past the next value, however deeply it's nested.

hammer_json_key_is

static inline int hammer_json_key_is(const char *key, uint64_t key_length, const char *literal);

Returns whether a key, or a string's raw contents, is exactly literal. The literal must not need any escaping.

hammer_json_unescape

static inline uint64_t hammer_json_unescape(const char *raw, uint64_t raw_length, char *out);

Decodes a string's raw contents into out as UTF-8 and returns the decoded length. The result is never longer than the raw contents, so out needs at most raw_length bytes. Surrogate pairs become a single character. It doesn't add a NUL.

char *name = hammer_must_alloc(raw_length + 1);
name[hammer_json_unescape(raw, raw_length, name)] = 0;

hammer_json_find

static inline int hammer_json_find(const char *object, uint64_t length, const char *key, HammerJson *json);

Finds key in the JSON object object. It starts *json on the object and leaves it at the key's value, ready for the readers above. Returns 0 if object is 0, isn't an object, or doesn't have the key.

HammerJson json;
int headings = hammer_json_find(settings.data, settings.length, "chapterHeadings", &json) && hammer_json_bool(&json);

Lower-level helpers

static inline void hammer_json_space(HammerJson *json);
static inline int hammer_json_expect(HammerJson *json, char c);
static inline uint32_t hammer_json_hex(const char *digits);

The readers are built from these. hammer_json_space moves past whitespace. hammer_json_expect skips any whitespace and moves past the character c; if something else is there, it sets error and returns 0. hammer_json_hex reads four hex digits, as found in a \u escape.

hammer_out.h

An output buffer that grows in hammer_alloc's memory, with functions for writing text, numbers, and JSON strings to it. Include it after hammer.h. HAMMER_IMPLEMENTATION defines its functions too.

static HammerBuffer out;
out = (HammerBuffer){0}; // at the start of each export, after hammer_alloc_reset()
hammer_put_text(&out, "{\"count\":");
hammer_put_int(&out, 12);
hammer_put_char(&out, '}');
extism_output_buf(out.data, out.length);

A buffer lives in hammer_alloc's memory, so if you keep one between exports, zero it before reusing it. Growing a buffer moves its data, so keep offsets into it rather than pointers. To undo everything written since a mark, set length back to the mark. Every writer calls hammer_fail with HAMMER_OUT_OF_MEMORY if memory runs out.

HammerBuffer

typedef struct {
  char *data;
  uint64_t length;
  uint64_t capacity;
} HammerBuffer;

data holds the length bytes written so far, with room for capacity bytes. A zeroed buffer, (HammerBuffer){0}, is empty and ready to write to.

hammer_reserve

void hammer_reserve(HammerBuffer *buffer, uint64_t extra);

Makes room for extra more bytes, doubling the capacity (starting from 4096) as needed. Growing copies the data to new memory, and the old block goes unused until the next reset. Call it before writing directly into data + length, then add what you wrote to length.

hammer_put, hammer_put_text, hammer_put_char

void hammer_put(HammerBuffer *buffer, const char *text, uint64_t length);
void hammer_put_text(HammerBuffer *buffer, const char *text);
void hammer_put_char(HammerBuffer *buffer, char c);

hammer_put appends length bytes of text, hammer_put_text appends a NUL-terminated string, and hammer_put_char appends one byte. All three write their input unchanged.

hammer_put_int

void hammer_put_int(HammerBuffer *buffer, int64_t n);

Appends n in decimal, with a - if it's negative.

hammer_put_json_string

void hammer_put_json_string(HammerBuffer *buffer, const char *text, uint64_t length);

Appends length bytes of text as a quoted JSON string, escaping ", \, and control characters. Pass it decoded text: the raw contents from hammer_json_string would be escaped twice.

hammer_terminated

const char *hammer_terminated(HammerBuffer *buffer);

Returns the buffer's text with a NUL after it. The NUL isn't counted in the buffer's length. Use it to pass a string you've built, such as hammer_call's input or a progress message, to a function that takes a C string.

hammer_text.h

Functions for stepping through UTF-8 and for working with letters, case, and words. Offsets are in bytes, which is what Hammer's diagnostics expect.

HammerText text = {paragraph, length};
uint64_t count;
HammerWord *words = hammer_words(text, &count);
if (count > 1 && hammer_same_word(text, words[0], words[1])) ...

Letters, combining marks, and the rest come from hammer_word_classes.h, a table of Unicode 17 that the Kotlin and Rust kits split words with too, so all three kits find the same words. Case is known for ASCII, Latin-1, Latin Extended-A, and Cyrillic; other letters have no case. Every function here is static inline.

HammerText

typedef struct {
  const char *data;
  uint64_t length;
} HammerText;

length bytes of UTF-8 at data, not necessarily NUL-terminated. Requests use it for strings, with data set to 0 for a string that wasn't sent.

HammerWord

typedef struct {
  uint64_t start;
  uint64_t end;
} HammerWord;

A word's position in a text: bytes [start, end).

ASCII character tests

static inline int hammer_is_lower(char c);
static inline int hammer_is_upper(char c);
static inline int hammer_is_ascii_letter(char c);
static inline int hammer_is_digit(char c);
static inline char hammer_to_lower(char c);
static inline char hammer_to_upper(char c);

Tests and case changes for a single ASCII byte. Any other byte isn't a letter, and the case functions return it unchanged.

hammer_decode

static inline uint32_t hammer_decode(const char *s, uint64_t available, uint64_t *length);

Returns the code point in the first available bytes of s, and sets *length to its length in bytes. For a continuation byte, or a character that's cut short, it returns U+FFFD with a length of one byte.

hammer_encode

static inline uint64_t hammer_encode(uint32_t code, char *out);

Writes a code point's UTF-8 to out, which must have room for 4 bytes, and returns how many bytes it wrote.

hammer_lower, hammer_upper

static inline uint32_t hammer_lower(uint32_t code);
static inline uint32_t hammer_upper(uint32_t code);

Converts a code point to lower case or upper case, for ASCII, Latin-1, Latin Extended-A, and Cyrillic. Other code points come back unchanged.

hammer_byte

static inline unsigned char hammer_byte(HammerText text, uint64_t i);

Returns the byte at i, or 0 past the end of the text.

hammer_char_is

static inline int hammer_char_is(HammerText text, uint64_t i, uint32_t code);

Returns whether the three bytes at i are the UTF-8 for code. code must be between U+0800 and U+FFFF.

hammer_char_at, hammer_char_length, hammer_char_before

static inline uint32_t hammer_char_at(HammerText text, uint64_t i, uint64_t *length);
static inline uint64_t hammer_char_length(HammerText text, uint64_t i);
static inline uint64_t hammer_char_before(HammerText text, uint64_t i);

hammer_char_at returns the code point at i and sets *length to its length in bytes; past the end, it returns 0. hammer_char_length returns the number of bytes in the character at i, which is 1 for ASCII, for stray continuation bytes, and for a character cut short by the end of the text. hammer_char_before returns the offset where the character before i starts; i must be greater than 0.

hammer_upper_at, hammer_lower_at

static inline int hammer_upper_at(HammerText text, uint64_t i);
static inline int hammer_lower_at(HammerText text, uint64_t i);

Return whether the character at i is upper case (one that hammer_lower changes) or lower case (one that hammer_upper changes). A letter without case is neither.

hammer_letter_at, hammer_letter_before

static inline int hammer_letter_at(HammerText text, uint64_t i);
static inline int hammer_letter_before(HammerText text, uint64_t i);

hammer_letter_at returns whether the character at i is a letter, counting ideographs as letters. hammer_letter_before returns whether there's a character before i and it's a letter.

Word classes

enum { HAMMER_OTHER, HAMMER_LETTER, HAMMER_MARK, HAMMER_IDEOGRAPH, HAMMER_JOINER };

What a code point is to hammer_words:

Class What it does in a word
HAMMER_LETTER Letters make words
HAMMER_MARK A combining mark stays on the letter before it
HAMMER_IDEOGRAPH Each ideograph (Han, kana, Bopomofo) is a word of its own
HAMMER_JOINER A joiner between two letters stays inside the word: an apostrophe (straight or U+2019), the Catalan middle dot, Hebrew's geresh or gershayim, ZWNJ, ZWJ, or a soft hyphen
HAMMER_OTHER Everything else, digits and hyphens among them, ends a word

hammer_word_class, hammer_word_class_at

static inline int hammer_word_class(uint32_t code);
static inline int hammer_word_class_at(HammerText text, uint64_t i);

hammer_word_class returns a code point's word class. ASCII letters are letters and the straight apostrophe is a joiner; every other code point is looked up in hammer_word_classes, and one it doesn't list is HAMMER_OTHER. hammer_word_class_at returns the class of the character at i, or HAMMER_OTHER past the end.

hammer_apostrophe_at

static inline uint64_t hammer_apostrophe_at(HammerText text, uint64_t i);

Returns the length in bytes of the apostrophe at i: 1 for a straight apostrophe, 3 for U+2019, or 2 for U+02BC, which Ukrainian uses. Returns 0 if there's no apostrophe there.

hammer_words

static inline HammerWord *hammer_words(HammerText text, uint64_t *count);

Splits a text into words, as the Kotlin and Rust kits' words() do. A word is a run of letters with their combining marks, and a joiner between two letters stays inside it, so "don't" is one word. Digits, hyphens, and everything else end a word. Chinese and Japanese put no spaces between words, so each ideograph is a word of its own, with any combining marks after it. *count is set to the number of words. The list is in hammer_must_alloc's memory, sized for the most words the text could contain.

hammer_word_length

static inline uint64_t hammer_word_length(HammerWord word);

Returns the word's length in bytes.

hammer_word_matches_case, hammer_word_matches

static inline int hammer_word_matches_case(HammerText text, HammerWord word, const char *literal, uint64_t n, int exact);
static inline int hammer_word_matches(HammerText text, HammerWord word, const char *literal, uint64_t n);

Return whether the word is the first n bytes of literal, ignoring case unless exact is set. An apostrophe in the literal, straight or curly, matches any apostrophe. hammer_word_matches always ignores case.

hammer_word_is

static inline int hammer_word_is(HammerText text, HammerWord word, const char *literal);

Returns whether the word is the NUL-terminated literal, ignoring case as hammer_word_matches does.

hammer_word_in

static inline int hammer_word_in(HammerText text, HammerWord word, const char *const *literals);

Returns whether the word is one of literals, a list ending with 0, such as one from hammer_resource_lines. Literals whose first ASCII letter differs from the word's are skipped without a full comparison.

hammer_word_starts_with

static inline int hammer_word_starts_with(HammerText text, HammerWord word, const char *prefix);

Returns whether the word begins with prefix, ignoring case.

hammer_same_word

static inline int hammer_same_word(HammerText text, HammerWord a, HammerWord b);

Returns whether two words of the text are the same, ignoring case.

hammer_all_upper

static inline int hammer_all_upper(HammerText text, HammerWord word);

Returns whether every letter of the word is upper case.

hammer_spaced

static inline int hammer_spaced(HammerText text, HammerWord a, HammerWord b);

Returns whether two words are separated only by spaces (at least one), so they read as one phrase.

hammer_match_phrase

static inline uint64_t hammer_match_phrase(HammerText text, const HammerWord *words, uint64_t count, uint64_t i,
                                           const char *phrase);

Returns how many words of phrase match, starting at words[i], or 0 if it doesn't match. The phrase's first word matches in either case, as it would at the start of a sentence, but the rest must match as written, so "de el" can match "de El Salvador". A * word matches any word. A space in the phrase matches spaces or one hyphen between words, and a hyphen matches only a hyphen.

uint64_t matched = hammer_match_phrase(text, words, count, i, "could of");
if (matched) hammer_report_fix(words[i].start, words[i + matched - 1].end, "Did you mean “could have”?", "could have", 10);

hammer_latin_a_capital

static inline int hammer_latin_a_capital(uint32_t code);

A helper for hammer_lower and hammer_upper. For a code point in Latin Extended-A, it returns 1 for an upper-case letter, -1 for a lower-case letter, and 0 for a letter without case.

hammer_word_classes.h

The table hammer_text.h classifies characters with, generated from Unicode 17.0.0 by the SDK's :codegen:generateWordClasses task. hammer_text.h includes it, and you don't need to include it yourself.

hammer_word_classes

static const uint32_t hammer_word_classes[][3] = { ... };

Runs of code points, in order, each as its first code point, its last, and its word class. A code point in no run is HAMMER_OTHER. Look a code point up with hammer_word_class instead of reading the table.

hammer_image.h

Finds project images in Markdown, without allocating. Hammer writes a project image as ![image](hammer://image/<id>), or with the destination in angle brackets, <hammer://image/<id>>; the alt text means nothing. It includes hammer_text.h.

uint64_t at = 0;
int32_t id;
while ((id = hammer_next_image_id(markdown, &at)) >= 0) ...

In an export with images = true, look the id up in the request's images. Anywhere else, get an image's bytes by its id with hammer_call and image.get.

HAMMER_IMAGE_PREFIX, HAMMER_IMAGE_PREFIX_LENGTH

#define HAMMER_IMAGE_PREFIX "hammer://image/"
#define HAMMER_IMAGE_PREFIX_LENGTH (sizeof HAMMER_IMAGE_PREFIX - 1)

What every image reference starts with, and its length in bytes.

hammer_image_id

static inline int32_t hammer_image_id(HammerText reference);

Returns the image id that a link's destination names: hammer://image/3, or <hammer://image/3>, is 3. Returns -1 for anything else, such as another scheme, or an id that's empty, isn't all ASCII digits, or is too large for an int32_t.

hammer_next_image_id

static inline int32_t hammer_next_image_id(HammerText text, uint64_t *at);

Returns the next image id that a Markdown text refers to, from byte *at on, and moves *at past it. When there are no more, it returns -1 and moves *at to the end. Start with *at at 0.

It finds references in images, angle-bracket destinations, reference definitions ([map]: hammer://image/3), and code alike, as Hammer counts them: in order, and once for each time they appear. A reference is hammer://image/ and its digits, with no letter, digit, _, +, ., or - just before it and no letter, digit, or _ just after, so hammer://image/12x names no image. Neither does an id too large for an int32_t.

Lower-level helpers

static inline int hammer_image_word_byte(unsigned char c);
static inline int hammer_image_prefix_at(HammerText text, uint64_t i);
static inline int32_t hammer_image_digits(HammerText text, uint64_t start, uint64_t end);

The functions above are built from these. hammer_image_word_byte returns whether a byte is an ASCII letter, a digit, or _. hammer_image_prefix_at returns whether hammer://image/ starts at byte i. hammer_image_digits returns the id in the ASCII digits [start, end), or -1 if there are none or the id is too large for an int32_t.

hammer_base64.h

Base64 in the standard alphabet with padding (RFC 4648, section 4), which is how an export's images and an archive's files carry their bytes. It allocates nothing and needs only <stdint.h>. Hammer's base64 contains nothing that JSON escapes, so the raw contents from hammer_json_string decode as they are.

const char *raw; uint64_t raw_length, size;
hammer_json_string(&json, &raw, &raw_length);
unsigned char *bytes = hammer_must_alloc(hammer_base64_size(raw_length));
if (!hammer_base64_decode(raw, raw_length, bytes, &size)) hammer_fail("An image was not base64");

hammer_base64_size

static inline uint64_t hammer_base64_size(uint64_t length);

Returns the most bytes that length characters of base64 can decode to. It can't overflow.

hammer_base64_decode

static inline int hammer_base64_decode(const char *text, uint64_t length, unsigned char *out, uint64_t *size);

Decodes length characters of base64 into out, which needs hammer_base64_size(length) bytes, and sets *size to how many bytes it wrote. Padding may be left out altogether. Returns 0 for text that isn't base64: a character outside the alphabet, padding anywhere but the end, more than two = or too few to fill the last group of four, or a last group of one character.

The bytes are never longer than the text and are written behind what has been read, so out may be the text itself, decoding it in place, when the text is your plugin's own copy of its input. A decode that fails partway leaves that text partly overwritten.

hammer_base64_encoded_size, hammer_base64_encode

static inline uint64_t hammer_base64_encoded_size(uint64_t length);
static inline void hammer_base64_encode(const unsigned char *bytes, uint64_t length, char *out);

hammer_base64_encoded_size returns how many characters hammer_base64_encode writes for length bytes. hammer_base64_encode writes length bytes as base64 with padding into out, which needs that many characters. It doesn't add a NUL.

hammer_base64_value

static inline int hammer_base64_value(char c);

Returns a character's six bits, or -1 for a character outside the alphabet. hammer_base64_decode is built on it.

hammer_archive.h

An archive exporter's reply. For a format whose manifest entry says output = "archive", the export export returns {"files": [...]}, and Hammer zips the files, in order, into the one export file. A file is text, bytes (as base64 "content"), or an image: the id of one of the request's images, whose bytes Hammer writes itself, so the reply carries none. Include it after hammer.h; it includes hammer_base64.h and hammer_out.h.

HammerArchive archive = {0};
hammer_archive_text(&archive, "story.md", story, story_length);
hammer_archive_image(&archive, "images/5.png", 5); // an export image's "id"
uint64_t length;
const char *reply = hammer_archive_finish(&archive, &length);
extism_output_buf(reply, length);

A path is relative, with / between its segments. It must not be empty, start with /, contain a \, have an empty segment (so no // and no trailing /), have a . or .. segment, or be a path already added. Adding a file at any other path calls hammer_fail with a message naming the path, since Hammer would refuse the reply. A path is NUL-terminated, while text and base64 take a length, so copy and terminate a path made from hammer_json_string's raw contents first.

Like a HammerBuffer, an archive lives in hammer_alloc's memory, so if you keep one between exports, zero it before reusing it.

HammerArchive

typedef struct {
  // The reply so far: empty until a file is added.
  HammerBuffer json;
  // Each path added, as its length's 8 bytes and then its own, for refusing one added twice.
  HammerBuffer paths;
  // Set by hammer_archive_finish, after which no file can be added.
  int finished;
} HammerArchive;

An archive being built. A zeroed archive, (HammerArchive){0}, is empty and ready for files.

hammer_archive_text

static inline void hammer_archive_text(HammerArchive *archive, const char *path, const char *text, uint64_t length);

Adds length bytes of UTF-8 text as the file at path.

hammer_archive_bytes

static inline void hammer_archive_bytes(HammerArchive *archive, const char *path, const unsigned char *bytes,
                                        uint64_t length);

Adds length bytes as the file at path, encoding them as base64.

hammer_archive_base64

static inline void hammer_archive_base64(HammerArchive *archive, const char *path, const char *text,
                                         uint64_t length);

Adds the file at path whose bytes are length characters of standard padded base64, written into the reply as they are. Use it for base64 your plugin built, or read from a resource. Hammer fails the export if they aren't base64.

hammer_archive_image

static inline void hammer_archive_image(HammerArchive *archive, const char *path, int32_t id);

Adds image id of the request's images as the file at path. Hammer writes its bytes, so the reply carries none. An id that Hammer didn't send fails the export.

hammer_archive_finish

static inline const char *hammer_archive_finish(HammerArchive *archive, uint64_t *length);

Ends the reply, once every file is added, and returns it for extism_output_buf, setting *length to its length. A second call returns it again. If no file was added, it calls hammer_fail, as does adding a file after it.

Lower-level helpers

static inline const char *hammer_archive_path_error(const char *path, uint64_t length);
static inline int hammer_archive_has(const HammerArchive *archive, const char *path, uint64_t length);
static inline void hammer_archive_start(HammerArchive *archive, const char *path, const char *key);

The functions above are built from these. hammer_archive_path_error returns the rule that length bytes of path break, such as "has an empty segment", or 0 if it breaks none; it doesn't check whether the path was added already. hammer_archive_has returns whether the archive has a file at that path already. hammer_archive_start checks a path and writes the file's entry up to the value of key.

hammer_action.h

Types and functions for the action export's request and replies. Include it after hammer.h; it includes hammer_json.h, hammer_out.h, and hammer_text.h itself. Everything here is static inline. The actions page explains pages, fields, buttons, and state.

HammerActionRequest request;
if (!hammer_action_read(input, size, &request)) hammer_fail("Hammer sent a request this could not read");
int64_t count = hammer_input_int(&request, "count", 8);

Strings in the request are JSON string contents exactly as Hammer sent them, escapes and all, and they point into the input. You can write them back into JSON unchanged; to decode one for display, use hammer_json_unescape. Keep the input alive (in hammer_alloc's memory, allocated after the reset) for as long as you use the request.

A reply is one of five kinds: a message, a document, a page of fields and buttons, errors, or an item to open. Write each reply with one of the hammer_put_ functions below.

HammerActionRequest

typedef struct {
  // The [[actions]] entry's name.
  HammerText action;
  HammerText project;
  // The screen it was run from: project, scene, note, entry, or event.
  HammerText place;
  // The id of the item that screen shows, when has_id is set.
  int64_t id;
  int has_id;
  // The id of the button pressed; data is 0 on the first call, which opens the action or runs the
  // manifest's [[actions.field]] page.
  HammerText button;
  // Reserved for fields that call the action when they change; data is 0.
  HammerText changed;
  // The UI's BCP 47 tag, for writing replies in the user's language; data is 0 when unknown.
  HammerText locale;
  // The input and settings objects' JSON: a value for each field of the page the user was on, and for
  // each declared setting.
  HammerText input;
  HammerText settings;
  // The last page's state: the JSON value it was written with. data is 0 on the first call, and after a
  // page without one.
  HammerText state;
  // Set on the first call after Hammer restored a run the user had left.
  int resumed;
} HammerActionRequest;

An action request. input, settings, and state each contain a whole JSON value; read them with hammer_json_init or the hammer_input functions. A HammerText whose data is 0 wasn't sent.

hammer_action_read

static inline int hammer_action_read(const char *text, uint64_t length, HammerActionRequest *request);

Reads the request in text into *request. Returns 0 if the request is malformed, or if it doesn't name an action or a project.

hammer_text_is

static inline int hammer_text_is(HammerText text, const char *literal);

Returns whether one of the request's strings, such as its action, place, or button, is literal. Returns false if the string wasn't sent.

if (hammer_text_is(request.action, "style-report")) ...
if (hammer_text_is(request.button, "next")) ...

hammer_input

static inline int hammer_input(const HammerActionRequest *request, const char *key, HammerJson *json);

Finds a field's value in the request's input and leaves *json at it, ready for the hammer_json_ readers. Returns 0 if the field isn't there.

HammerJson json;
if (hammer_input(&request, "scenes", &json) && hammer_json_enter_array(&json)) {
  while (hammer_json_next_item(&json)) {
    int64_t id = hammer_json_int(&json);
    ...
  }
}

hammer_input_bool, hammer_input_int

static inline int hammer_input_bool(const HammerActionRequest *request, const char *key, int fallback);
static inline int64_t hammer_input_int(const HammerActionRequest *request, const char *key, int64_t fallback);

Return a bool or int field's value, or fallback if the field is missing or has a value of another type.

hammer_input_string

static inline HammerText hammer_input_string(const HammerActionRequest *request, const char *key);

Returns a text or choice field's value as Hammer sent it (raw, escapes included). data is 0 if the field has no value.

hammer_entity_read

static inline int hammer_entity_read(HammerJson *json, HammerText *kind, int64_t *id);

Reads the value at json as an entity field's pick, such as {"kind":"entry","id":12}, and moves past it. It gives you the pick's kind (scene, note, entry, or event) and its id. For null, or anything else that isn't a pick, it returns 0 and zeroes kind and id. For a field with multiple, step through its list with hammer_json_enter_array and hammer_json_next_item, and call this for each item.

hammer_input_entity

static inline int hammer_input_entity(const HammerActionRequest *request, const char *key, HammerText *kind, int64_t *id);

Reads a single entity field's pick: it sets the kind and id, and returns 1. If the field is null or isn't there, it returns 0 and zeroes both.

HammerText kind;
int64_t id;
if (hammer_input_entity(&request, "who", &kind, &id) && hammer_text_is(kind, "entry")) ...

HammerFieldType

typedef enum {
  HAMMER_FIELD_TEXT,
  HAMMER_FIELD_BOOL,
  HAMMER_FIELD_INT,
  HAMMER_FIELD_CHOICE,
  HAMMER_FIELD_SCENES,
  HAMMER_FIELD_ENTITY
} HammerFieldType;

A field's type. The reply writes the types as text, bool, int, choice, scenes, and entity.

HammerTextFormat

typedef enum { HAMMER_TEXT_PLAIN, HAMMER_TEXT_RICH, HAMMER_TEXT_RICH_EXTENDED } HammerTextFormat;

Which editor a text field uses. HAMMER_TEXT_PLAIN is an ordinary text field. The other two use the scene editor, whose value is markdown: HAMMER_TEXT_RICH with its format bar, and HAMMER_TEXT_RICH_EXTENDED with the extended bar (highlight, task lists, tables). The reply writes them as plain, rich, and rich-extended. HAMMER_TEXT_PLAIN is 0, so it's left out.

HammerOption

typedef struct {
  const char *value;
  const char *label;
} HammerOption;

One option of a choice field. value is what's sent back, and label is what the writer sees.

HammerField

typedef struct {
  HammerFieldType type;
  const char *key;
  const char *label;
  const char *hint;
  // Text must not be blank, scenes empty, or an entity unpicked; Hammer checks before calling.
  int required;
  // The initial value as JSON, such as "true", "8", "\"elvish\"", "[3,4]", or
  // "{\"kind\":\"entry\",\"id\":12}"; 0 for the type's own: false, min or 0, the first option, "", or
  // none.
  const char *default_json;
  // An int's bounds, each used when has_min or has_max is set.
  int64_t min;
  int64_t max;
  int has_min;
  int has_max;
  // A choice's options, at least one.
  const HammerOption *options;
  uint32_t option_count;
  HammerTextFormat format;
  // A plain text field of several lines.
  int multiline;
  // Scenes as one scene instead of a list; only scenes read it.
  int single;
  // An entity's kinds of item: scene, note, entry, or event; all four when kind_count is 0.
  const char *const *kinds;
  uint32_t kind_count;
  // An entity's types of entry, with entry among its kinds: person, place, thing, event, or idea; any
  // when entry_type_count is 0.
  const char *const *entry_types;
  uint32_t entry_type_count;
  // Tags an entity's item needs one of; any item when tag_count is 0.
  const char *const *tags;
  uint32_t tag_count;
  // An entity as a list of picks instead of one; only an entity reads it, since scenes are a list unless
  // single.
  int multiple;
} HammerField;

A field on a page. Its value comes back in the next request's input under key, in a form that depends on the field's type:

  • text and choice: a string
  • bool: a bool
  • int: an integer
  • scenes: a list of scene ids, or one id or null when single is set
  • entity: one pick, such as {"kind":"entry","id":12}, or null; a list of picks when multiple is set

Members left at 0 are left out of the reply, so a designated initializer only needs to name the members you use. default_json is written exactly as given, so it must be valid JSON.

static const char *const entries[] = {"entry"};
static const char *const people[] = {"person"};
HammerField who = {.type = HAMMER_FIELD_ENTITY, .key = "who", .label = "Who", .required = 1,
                   .kinds = entries, .kind_count = 1, .entry_types = people, .entry_type_count = 1};

HammerButton

typedef struct {
  const char *id;
  const char *label;
  int primary;
  int is_inline;
  int no_validate;
} HammerButton;

A button on a page. Pressing it calls the action again, with id as button, the page's values as input, and the page's state as state. A button marked is_inline sits under the markdown, for acting on something in it; the other buttons sit in the dialog's footer. no_validate skips Hammer's checks of the fields, for a button such as Back.

HammerPage

typedef struct {
  const char *title;
  const char *markdown;
  uint64_t markdown_length;
  const HammerField *fields;
  uint32_t field_count;
  const HammerButton *buttons;
  uint32_t button_count;
  const char *state;
  uint64_t state_length;
} HammerPage;

What the dialog shows. Every member may be 0. markdown_length and state_length are the lengths of markdown and state; 0 means the string runs up to its NUL. state can be any JSON value. It's written into the reply exactly as given, and Hammer passes it back as the next request's state without ever looking inside it. If a call's state is larger than 1 MiB, the call fails.

HammerField fields[] = {{.type = HAMMER_FIELD_TEXT, .key = "name", .label = "Name", .required = 1}};
HammerButton buttons[] = {{.id = "back", .label = "Back", .no_validate = 1},
                          {.id = "next", .label = "Next", .primary = 1}};
HammerPage page = {.title = "Who?", .fields = fields, .field_count = 1,
                   .buttons = buttons, .button_count = 2, .state = "{\"step\":2}"};
hammer_put_page(&out, &page, 0);

HammerError

typedef struct {
  const char *field;
  const char *message;
} HammerError;

An error message for one field, named by its key, or for the whole page when field is "" or 0.

hammer_put_page

static inline void hammer_put_page(HammerBuffer *out, const HammerPage *page, const char *message);

Writes a reply that shows page, plus a message to show briefly (0 for none). If page is 0, the reply has no page and the action ends.

hammer_put_message

static inline void hammer_put_message(HammerBuffer *out, const char *message);

Writes a reply that ends the action and briefly shows message. If message is 0, the reply is {}, which ends the action without showing anything.

hammer_put_document

static inline void hammer_put_document(HammerBuffer *out, const char *markdown, uint64_t length);

Writes a reply that shows length bytes of markdown, as a report the writer can copy or save as a note. The markdown doesn't need to end with a NUL.

hammer_put_errors

static inline void hammer_put_errors(HammerBuffer *out, const HammerError *errors, uint32_t count, const char *message);

Writes a reply that keeps the writer on the page they were on, with what they entered and the page's state. Each error appears under its field, and message shows briefly (0 for none). If there are no errors, or no page to stay on, the action ends.

HammerError errors[] = {{.field = "name", .message = "That name is taken"}};
hammer_put_errors(&out, errors, 1, 0);

hammer_put_open

static inline void hammer_put_open(HammerBuffer *out, const char *place, int64_t id, int has_id, const char *message);

Writes a reply that ends the action and opens an item. place is project, scene, note, entry, or event. id is the item's id, used when has_id is set (never for project). message shows briefly (0 for none).

Reply building blocks

static inline void hammer_put_json_text(HammerBuffer *out, const char *text);
static inline void hammer_put_json_texts(HammerBuffer *out, const char *key, const char *const *texts, uint32_t count);
static inline void hammer_put_field(HammerBuffer *out, const HammerField *field);
static inline void hammer_put_button(HammerBuffer *out, const HammerButton *button);
static inline void hammer_put_reply_end(HammerBuffer *out, const char *message, int first);

The writers above are built from these.

  • hammer_put_json_text appends a NUL-terminated string as a JSON string.
  • hammer_put_json_texts appends key, written exactly as given (such as ",\"tags\":"), and then a JSON list of count strings. It appends nothing when count is 0.
  • hammer_put_field and hammer_put_button each append one field or button object.
  • hammer_put_reply_end appends message, if there is one, and the closing brace. Set first when nothing came before it in the object.

hammer_diagnose.h

A diagnose export for the checks the manifest declares under [[diagnostics]]. The kit reads the request and writes the reply; your plugin defines two functions. Include this header after hammer.h. In the file that defines HAMMER_IMPLEMENTATION, it defines the export, which resets hammer_alloc's memory, reads the input, and calls hammer_diagnose_run. Text checks describes the export's output.

HammerDiagnoseRequest

typedef struct {
  // The [[diagnostics]] entry's name.
  HammerText name;
  // The project's language as sent, such as "en" or "en-US"; data is 0 when Hammer has none.
  HammerText language;
  // The settings object's JSON; data is 0 when there is none.
  HammerText settings;
  // The UI's BCP 47 tag, for writing messages in the user's language; data is 0 when unknown. Not the
  // project's language, which is what the text is written in.
  HammerText locale;
  // The name of the project the text belongs to, for operations such as entry.list; data is 0 when
  // Hammer sent none.
  HammerText project;
} HammerDiagnoseRequest;

A diagnose request, without its paragraphs. The paragraphs arrive one at a time at hammer_diagnose_paragraph. If your plugin has several [[diagnostics]] entries, tell them apart by name. project names the project for operations that take one, such as entry.list through hammer_call.

hammer_diagnose_begin

int hammer_diagnose_begin(const HammerDiagnoseRequest *request);

You define this function. The kit calls it once per request, before any paragraph, so read the settings and the language here. Return 0 to report nothing, for example for a language your plugin doesn't check. It isn't called for a request with no paragraphs.

int hammer_diagnose_begin(const HammerDiagnoseRequest *request) {
  settings.long_sentences = hammer_setting_bool(request, "longSentences", 0);
  settings.long_sentence_words = hammer_setting_int(request, "longSentenceWords", 40);
  return hammer_language_is(request, "en");
}

hammer_diagnose_paragraph

void hammer_diagnose_paragraph(HammerText paragraph);

You define this function. The kit calls it for each paragraph, as plain UTF-8 already decoded from JSON. Report a mistake with hammer_report, and something the writer may want to change with hammer_suggest. The paragraph is in hammer_must_alloc's memory.

HammerFix

typedef struct {
  const char *replacement;
  uint64_t length;
  const char *label;
} HammerFix;

A fix: length bytes of replacement text for the diagnostic's range. Hammer shows the replacement itself, or label (a NUL-terminated string) if you give one. Give a label when the replacement alone wouldn't say what the fix does, such as an empty replacement that removes a word.

hammer_report

void hammer_report(uint64_t start, uint64_t end, const char *message, const HammerFix *fixes, uint32_t fix_count);

Reports bytes [start, end) of the current paragraph as a mistake, which Hammer marks with a wavy underline. It takes a NUL-terminated message and fix_count fixes (0 for none). Call it only from hammer_diagnose_paragraph. The strings are copied into the reply immediately, so they don't need to outlive the call.

hammer_suggest

void hammer_suggest(uint64_t start, uint64_t end, const char *message, const HammerFix *fixes, uint32_t fix_count);

Works like hammer_report, for something the writer may want to change rather than a mistake. Hammer marks it with a quieter, dotted underline. The reply gives it "severity": "suggestion".

hammer_report_fix

static inline void hammer_report_fix(uint64_t start, uint64_t end, const char *message, const char *replacement,
                                     uint64_t length);

Calls hammer_report with one plain fix of length bytes, or with no fix if replacement is 0.

hammer_report_fix(a.start, b.end, "Repeated word", text.data + a.start, hammer_word_length(a));

hammer_suggest_fix

static inline void hammer_suggest_fix(uint64_t start, uint64_t end, const char *message, const char *replacement,
                                      uint64_t length);

Calls hammer_suggest with one plain fix of length bytes, or with no fix if replacement is 0.

hammer_diagnose_run

const char *hammer_diagnose_run(const char *input, uint64_t size, uint64_t *reply_length);

Reads a request and returns the reply, {"diagnostics": [...]}, setting *reply_length to its length. The export calls it, and native tests call it through HAMMER_EXPECT. Call hammer_alloc_reset() first, and make sure input is either outside hammer_alloc's memory or allocated after the reset. A malformed request ends the call with hammer_fail. The reply is in hammer_alloc's memory.

hammer_setting

static inline int hammer_setting(const HammerDiagnoseRequest *request, const char *key, HammerJson *json);

Finds a setting's value in the request and leaves *json at it, ready for the hammer_json_ readers. Returns 0 if the setting isn't there.

hammer_setting_bool, hammer_setting_int

static inline int hammer_setting_bool(const HammerDiagnoseRequest *request, const char *key, int fallback);
static inline int64_t hammer_setting_int(const HammerDiagnoseRequest *request, const char *key, int64_t fallback);

Return a bool or int setting's value, or fallback if the setting is missing or has a value of another type.

hammer_language_is

static inline int hammer_language_is(const HammerDiagnoseRequest *request, const char *code);

Returns whether the request's language is code, such as "en", with or without a region ("en-US", "en_GB"). code must be lower case; the request's language is compared ignoring case.

hammer_test.h

Native unit tests. This header works only with HAMMER_NATIVE defined; otherwise it stops the compile with an error. Your plugin's test.c includes the plugin's source and then this header, and test.sh builds and runs it.

#include "simple-grammar.c"
#include "hammer_test.h"

static void articles_test(void) {
  HAMMER_EXPECT(0, "It was a apple. An dog barked.", "[a]>an; [An]>A");
  HAMMER_EXPECT(0, "A one-off, a unicorn, an hour, a European.", "");
}

int main(void) {
  articles_test();
  return hammer_test_summary();
}

The HAMMER_EXPECT macros exist only if hammer_diagnose.h was included before this header. HAMMER_CHECK and hammer_test_summary are always available. The header's other static functions are the macros' internals.

HAMMER_CHECK

#define HAMMER_CHECK(condition) hammer_test_check((condition), #condition, __FILE__, __LINE__)

Counts a check. If condition is false, it prints the file, line, and condition to standard error. Use it for anything that isn't a diagnose finding, such as reading an action request or a reply's JSON.

HammerActionRequest request;
HAMMER_CHECK(hammer_action_read(later, strlen(later), &request));
HAMMER_CHECK(request.has_id && request.id == 12);

HAMMER_TEST_DIAGNOSTICS

#ifndef HAMMER_TEST_DIAGNOSTICS
#define HAMMER_TEST_DIAGNOSTICS "grammar"
#endif

The [[diagnostics]] name that the HAMMER_EXPECT macros put in their requests. To change it, define it before including this header.

HAMMER_EXPECT, HAMMER_EXPECT_IN

#define HAMMER_EXPECT(settings, paragraph, expected) \
  hammer_test_expect("en", (settings), (paragraph), (expected), 0, __FILE__, __LINE__)
#define HAMMER_EXPECT_IN(language, settings, paragraph, expected) \
  hammer_test_expect((language), (settings), (paragraph), (expected), 0, __FILE__, __LINE__)

Runs a whole diagnose request for one paragraph and compares the findings with expected. HAMMER_EXPECT uses English, and HAMMER_EXPECT_IN uses language. settings is the settings object's JSON, or 0 for {}.

In expected, each finding reads [text]>fix|fix, where text is what the finding covers. A fix with a label reads replacement{label}. A suggestion, from hammer_suggest, has a ~ after its text: [text]~>fix. Findings are separated by ; , and "" means no findings. On a mismatch, the test prints the paragraph, what was expected, and what came back.

HAMMER_EXPECT(0, "Could of been worse, alot worse.", "[Could of]>Could have; [alot]>a lot");
// OPTIONAL_ON is the test's own settings JSON, turning on the rules that are off by default.
HAMMER_EXPECT(OPTIONAL_ON, "In order to win, we got a free gift!!", "[In order to]>To; [free gift]>gift; [!!]>!");
HAMMER_EXPECT_IN("fr", 0, "Nous nous sommes vus, vous vous êtes tus, et le le chat.", "[le le]>le");

HAMMER_EXPECT_MESSAGES, HAMMER_EXPECT_MESSAGES_IN

#define HAMMER_EXPECT_MESSAGES(settings, paragraph, expected) \
  hammer_test_expect("en", (settings), (paragraph), (expected), 1, __FILE__, __LINE__)
#define HAMMER_EXPECT_MESSAGES_IN(language, settings, paragraph, expected) \
  hammer_test_expect((language), (settings), (paragraph), (expected), 1, __FILE__, __LINE__)

These work like HAMMER_EXPECT, but each finding shows its message after its text instead of its fixes: "[the the] Repeated word", or for a suggestion, "[very ]~ “Very” rarely helps".

hammer_test_summary

static int hammer_test_summary(void);

Prints how many checks passed, or how many of them failed, and returns the exit status for main: 0 if all passed, 1 otherwise.

Other kits

The same plugins can be written with the Kotlin kit or the Rust kit.