Native C runner API

For authors writing a workflow runner in C. The C SDK is the same authoring surface as the Python and Bash ones, in a clean C-idiomatic ABI. Like the Bash library it is a bridge client: every verb execs $HTTK_WORKFLOW_PYTHON -m httk.workflow._shell_bridge <verb> , which drives the same Attempt object the Python SDK exposes, so a C runner and a Python or Bash runner publish the same bytes for the same campaign. Only the --describe handshake is native. The normative cross-language semantics are the table in Python and Bash authoring parity; the function-by-function C mapping is the table below.

The SDK is one header/source pair, httk_workflow.h and httk_workflow.c, packaged under httk.workflow at native/c/. It is C99 with no dependency beyond libc and POSIX, and it is designed to be vendored into a runner’s own tree or compiled directly beside it. It is also the foundation the later Fortran bindings build on: the exported symbols are all prefixed httk_workflow_, take opaque scalars and NUL-terminated strings, and never a macro where a function works.

A complete runner

A C runner declares its workflow and its complete step set once, implements one handler per step, and hands control to httk_workflow_main:

#include "httk_workflow.h"

static int step_prepare(void) { /* … */ httk_workflow_advance("run", NULL); return 0; }
static int step_run(void)     { /* … */ httk_workflow_succeed();          return 0; }

int main(int argc, char **argv) {
    static const httk_workflow_step steps[] = {
        {"prepare", step_prepare},
        {"run",     step_run},
    };
    if (httk_workflow_runner("my.workflow", steps, 2) != 0)
        return 2;
    return httk_workflow_main(argc, argv);
}

Build it against the packaged SDK:

cc -std=c99 -Wall -Wextra runner.c \
   .../httk/workflow/native/c/httk_workflow.c \
   -I.../httk/workflow/native/c -o runner

The compiled binary is the runner file a job references. It is executable, so the manager runs it directly and the scaffolder describes it by running it — a job that starts from a runner file of your own is resolved the same way whatever language wrote it.

A complete VASP relaxation authored this way — prepare, run, publish, the vasp.command setting, mock-VASP compatible, publishing to transactional data — ships as examples/relax_c/; see the walkthrough at the end of this page.

Registration and dispatch

httk_workflow_runner(workflow_id, steps, count) declares the complete step set before any work happens. Each httk_workflow_step pairs a step name with the int (*)(void) handler that implements it. A name that is empty, contains a character outside [A-Za-z0-9._-], is duplicated, or has a NULL handler is refused with a diagnostic on stderr and the return value HTTK_WORKFLOW_REFUSED. The registration is the one piece of state the library keeps, mirroring the Bash library’s private globals; the steps array must outlive the process, so a static const array is idiomatic.

httk_workflow_main(argc, argv) reads the step the manager asked for, dispatches its handler, and owns the process exit status — which is why the handlers and the outcome functions return rather than exit. It turns every ending of a step into exactly one outcome, the same guarantee the other SDKs give:

Ending

Published outcome

the handler publishes one

that outcome

the handler returns 0 without publishing

fail("no_outcome", …)

the step is not registered

fail("unknown_step", "… registered steps: …")

the handler returns nonzero

an error.json breadcrumb (exception CError), then that nonzero exit the manager records as process_failure

A handler returns 0 when it ended — whether or not it published — and a nonzero value when it could not complete. The nonzero case is the C analogue of a Bash handler dying under set -e: the unpublished draft is discarded, error.json records the step, the message, and the status, and the process exits nonzero. There is no per-line breadcrumb, because C has no shell ERR trap; the message names the step and its status.

HTTK_WORKFLOW_DESCRIBE=1 makes httk_workflow_runner print the runner description and exit 0 before any step runs, and httk_workflow_main --describe does the same. The description is produced natively, byte-for-byte what a Python or Bash runner prints for the same workflow and steps:

{"format": "httk-workflow-runner-description", "format_version": 1, "steps": ["prepare", "run"], "workflow": "my.workflow"}

The step names are byte-sorted; the workflow follows.

Memory ownership

Every function that returns a char * returns a freshly malloc’d, NUL-terminated string the caller must free(), or NULL. NULL means the answer is absent or the call was refused; the int *status out-parameter (when present) carries the bridge exit status that distinguishes the two — see the exit codes below. Trailing newlines are stripped from a captured value, as command substitution does in the shell.

Functions that return int return the bridge exit status directly. The variadic-tail parameters spelled const char *const *args are NULL-terminated arrays of extra bridge arguments the caller owns; pass NULL for none. They carry the same options the corresponding Bash function forwards untouched (for example --step, --parameter NAME=VALUE, --when, --priority).

httk_workflow_invoke(out, argv) is the foundation every verb is built on and the escape hatch for any bridge subcommand or option without a dedicated wrapper. argv names the subcommand and its arguments; the SDK prepends the interpreter invocation. When out is non-NULL the subcommand’s stdout is captured into *out; when it is NULL, stdin and stdout are inherited so run and batch stream normally.

The C function table

Each C function is one bridge subcommand, the same subcommand the paired Bash function calls, so this table’s Bash and Python columns are the Python and Bash authoring parity rows this SDK realizes. args is a NULL-terminated options array; status is the bridge exit status out-parameter.

C

Bash

Python

httk_workflow_runner(id, steps, n)

httk_workflow_runner

Runner

httk_workflow_main(argc, argv)

httk_workflow_main

Runner.main

httk_workflow_describe()

httk_workflow_main --describe

Runner.description

httk_workflow_invoke(out, argv)

_httk_workflow_bridge

shell_bridge.main

httk_workflow_context(field, status)

httk_workflow_context

Attempt.context

httk_workflow_parameter(name, fallback, status)

httk_workflow_parameter

Attempt.parameter

httk_workflow_setting(name, fallback, status)

httk_workflow_setting

Attempt.setting

httk_workflow_environment(name, fallback, status)

httk_workflow_environment

Attempt.environment

httk_workflow_state_get(name, status)

httk_workflow_state_get

JobState.read

httk_workflow_state_set(name, value)

httk_workflow_state_set

JobState.set

httk_workflow_state_delete(name)

httk_workflow_state_delete

JobState.delete

httk_workflow_state_merge(assignments)

httk_workflow_state_merge

JobState.merge

httk_workflow_declare(name, file)

httk_workflow_declare

Attempt.declare

httk_workflow_declaration(name, status)

httk_workflow_declaration

Attempt.declaration

httk_workflow_runlog_note(message)

httk_workflow_runlog_note

RunLog.append

httk_workflow_runlog_headline(message)

httk_workflow_runlog_headline

RunLog.append

httk_workflow_runlog_append(message, files)

httk_workflow_runlog_append

RunLog.append

httk_workflow_log(level, message)

httk_workflow_log

— (logging)

httk_workflow_put(source, dest, status)

httk_workflow_put

Attempt.put

httk_workflow_remove(dest, missing_ok, status)

httk_workflow_remove

Attempt.remove

httk_workflow_spawn(label, args, status)

httk_workflow_spawn

Attempt.spawn

httk_workflow_children(selection, status)

httk_workflow_children

Attempt.children

httk_workflow_child(label, field, status)

httk_workflow_child

ChildResult

httk_workflow_advance(next_step, args)

httk_workflow_advance

Attempt.advance

httk_workflow_gather(next_step, args)

httk_workflow_gather

Attempt.gather

httk_workflow_succeed()

httk_workflow_succeed

Attempt.succeed

httk_workflow_fail(code, message, args)

httk_workflow_fail

Attempt.fail

httk_workflow_retry(reason)

httk_workflow_retry

Attempt.retry

httk_workflow_pause(reason)

httk_workflow_pause

Attempt.pause

httk_workflow_batch()

httk_workflow_batch

httk_workflow_job_prepare(dest, spec, status)

httk_workflow_job_prepare

prepare_job_payload

httk_workflow_workdir_apply(spec, status)

httk_workflow_workdir_apply

Attempt.workdir_batch

httk_workflow_run(args)

httk_workflow_run

ProcessSupervisor

httk_calc(expression, status)

httk_calc

evaluate_expression

httk_template_render(template, output, values)

httk_template_render

render_template

httk_compress(args)

httk_compress

compress_files

httk_decompress(args)

httk_decompress

decompress_files

The httk_vasp_* surface of the Bash SDK has no dedicated C wrappers; a C runner that needs a VASP subcommand reaches it through httk_workflow_invoke with the same vasp-* verb, which is why the example below runs the configured command through httk_workflow_run and classifies its result.

Exit codes

Every function reports the same three-status discipline as Bash, so an absent answer never looks like a broken call:

Status

Meaning

HTTK_WORKFLOW_OK (0)

the call succeeded

HTTK_WORKFLOW_ABSENT (1)

the answer is legitimately absent: an unset state key, a missing parameter without a default, a child that was not observed

HTTK_WORKFLOW_REFUSED (2)

the call is refused: bad usage, a protocol violation, a corrupt attempt context — also what is returned when HTTK_WORKFLOW_PYTHON is unset

Reading a value is therefore an ordinary conditional:

int status;
char *energy = httk_workflow_state_get("energy", &status);
if (status == HTTK_WORKFLOW_OK) {
    /* resume from *energy */
}
free(energy);

httk_workflow_run returns the classified outcome of the program it ran instead: 0, 22 for a nonzero exit, 124 for a timeout whose process group was terminated, and 125 when a checker or diagnostic stopped it.

The examples/relax_c walkthrough

examples/relax_c/relax.c has the same three-step shape as the packaged Bash runner vasp_relax.shprepare, run, publish — built entirely on the functions above. It is a teaching example, not a drop-in replacement for the packaged runner: it deliberately omits the input derivation (vasp-prepare, KPOINTS/POTCAR generation), the finer run classifications, the reviewed remedy ladder, the parsed energy state, and the POTCAR.provenance.json the real runner produces, so it collapses run to “completed or vasp.failed”. What it does share is the protocol machinery — the vasp.command setting, mock-VASP compatibility, structured failure codes, and publishing to transactional data:

  • prepare stages the payload POSCAR into the workdir with httk_workflow_parameter("poscar", "files/POSCAR", …) and copy_file, fails by name (httk_workflow_fail("vasp.input_missing", …)) when it is absent, copies an optional INCAR, notes progress with httk_workflow_runlog_note, and httk_workflow_advance("run", NULL).

  • run resolves the VASP command with httk_workflow_setting("vasp.command", …) — falling back to a vasp_command parameter, and failing vasp.command_missing when neither is set — runs it under supervision with httk_workflow_run, records state_set("classification", "completed") and advances to publish on success, and httk_workflow_fail("vasp.failed", …) otherwise.

  • publish stages the finished files into the job’s transactional data with httk_workflow_put when the job has a data directory, and httk_workflow_succeed.

Build and describe it:

cd examples/relax_c
make
./relax --describe

Then drive it, naming the compiled binary as the runner and the mock VASP as the command. The runner starts at prepare and reads the structure from files/POSCAR rather than a declared input, so the job names the step explicitly and stages the file:

httk project init --name relax-c
httk workflow workspace init . --name default
httk workflow job new --workflow ./relax --step prepare --file POSCAR=POSCAR --data-mode transactional --tag silicon
httk workflow workspace settings set vasp.command "$PWD/../mock_vasp.py"
httk workflow run
httk workflow collect

The finished calculation lands in jobs/*/data/vasp/. It publishes the same protocol artifacts a Python or Bash runner does — outcome, transactional data, run log — through the one shared implementation; it does not reproduce the packaged VASP runner’s derived inputs and richer state (see above).