Skip to content
BlueShank edited this page Sep 1, 2026 · 5 revisions

Crash capture exposes a public C interface so other binary modules can integrate with it:
add your own report sections, share key/value data, query liveness, and request dumps.
Everything lives in include/crashcapture_api.h, a single header with no dependencies.

The interface name is CRASHCAPTURE001, version CC_API_VER 1.

Discovery

You never link against crash capture; the header ships a static CC_FIND_API() that scans the loaded modules for a crash capture binary (by known filenames and by any module whose name contains crashcapture) and calls its CreateInterface("CRASHCAPTURE001").

  • CC_FIND_API(): CCAPI* - locate the loaded crash capture and get its CCAPI table, or NULL if it isn't loaded yet.
  • CC_ENSURE_API(): CCAPI* - convenience wrapper: returns the api only if CC_API_OK passes, else NULL.
  • CC_API_OK(api, minSize): bool - true when the api is present, at least version 1, and big enough to contain the first minSize bytes.
    • Pass sizeof(CCAPI) for everything, or the size of a trailing subset you actually use, so older hosts stay compatible.

Because crash capture may load after your module, call CC_ENSURE_API() on your game-thread tick until it succeeds.
This "discovery + retry" pattern is the expected usage.

#include "crashcapture_api.h"

static CCAPI* cc = nullptr;

static bool EnsureApi()
{
    if (cc) return true;
    cc = CC_ENSURE_API();
    return cc != nullptr;
}

Reference

All functions below are members of the CCAPI struct, reached as cc->method(...).
The header marks each one with the threads it is safe on; they are grouped that way here.

Any thread

  • cc->version_str(): const char*
    • Crash capture version string (static).
  • cc->build_str(): const char*
    • Build string (static).
  • cc->side_str(): const char*
    • "server" or "client" (static).
  • cc->os_str(): const char*
    • Operating system (static).
  • cc->arch_str(): const char*
    • Architecture (static).
  • cc->is_hung(): int
    • 1 while the watchdog is handling a freeze (heartbeat and Windows window-probe detectors), 0 otherwise and before init.
  • cc->hung_since_ms(): unsigned long long
    • Milliseconds since the watchdog declared the current freeze, 0 if not hung.
  • cc->stall_class(): int
    • What the hung thread is stuck in: STALL_UNKNOWN=0, STALL_NATIVE=1, STALL_PHYSICS=2, STALL_LUA_INTERP=3, STALL_LUA_JIT=4.
  • cc->stall_class_name(int cls): const char*
    • "unknown"/"native"/"physics"/"lua"/"lua-jit" for a class number.
  • cc->ms_since_pulse(): unsigned long long
    • Milliseconds since the last heartbeat pulse, 0 if none has happened yet.
    • Build your own liveness logic on this if is_hung is too unstable.
  • cc->config_get(const char* name): const char*
    • Read-only lookup of launch/runtime settings (CRASHCAPTURE_* names, with or without the prefix), NULL for unknown keys.
  • cc->report_dir(): const char*
    • Report folder as configured (may be relative).
  • cc->log_path(): const char*
    • Path of the report/session file currently open, else NULL.
    • The buffer is only valid until the next report opens, copy it if you keep it.
  • cc->uptime_ms(): unsigned long long
    • Milliseconds since crash capture initialized.
  • cc->map_name(): const char*
    • Current map name, NULL when unknown.
  • cc->dump(const char* reason): int
    • Returns 1 written, 0 skipped (not initialized, or another dump/report already in flight - fail-fast, never blocks).
    • reason is truncated to 256 chars.
    • Any live thread, never a crash context.
  • cc->grace(int seconds): void
    • Suppress freeze detection for the next N seconds (planned heavy work).
    • Clamped to 0 < seconds <= 86400.
  • cc->backtrace(unsigned long long* out, int max): int
    • Walk the calling thread's native stack (return addresses), up to max, returns the count written.
    • Live threads only, not from crash/signal context.
  • cc->resolve(unsigned long long addr, char* out, unsigned outsz): int
    • Symbolize one address ("name+0xoff (file:line)" when debug info allows).
    • Returns 1 wrote a name, 0 failed or the symbol engine is busy (shared with the report path, fail-fast, never blocks).

Game thread only

  • cc->pulse(): void
    • Feed the heartbeat (only useful if crash capture is not already pulsing via the plugin GameFrame or the Lua timer).
  • cc->set_data(const char* key, const char* value): int
    • Register a key/value line shown in the report's "Third-party data" section.
    • Both strings are copied (key <= 47 chars, value <= 255 chars) and stay until overwritten or cleared.
    • Returns 1 stored, 0 table full.
  • cc->clear_data(const char* key): void
    • Remove a key previously set with set_data.
  • cc->add_section(const char* name, CCSectionFn fn, void* user): int
    • Register a report section that runs at report-write time.
  • cc->remove_section(const char* name): void
    • Unregister a section added with add_section.
    • Call before your module unloads, or its callback pointer dangles.

Report sections

A section is a callback that runs at report-write time (crash, freeze, or dump).
Its output lands in the report under ## <name>.

The callback runs wrapped in crash capture's crash protection: if it faults, the section is skipped so the report survives.
That protection only helps if the callback stays minimal - no allocation, no locks, no engine calls.
Write the section through the writer you are given:

  • w->print(ctx, const char* text)
    • append one or more lines of text (include your own \n).
  • w->format(ctx, const char* fmt, ...)
    • printf-style append; output is truncated to ~1024 chars per call.

The void* user you pass to add_section is round-tripped back to your callback untouched.

Warning

Not calling remove_section before your module unloads can lead to crashes if the callback pointer is freed or moved.

Example

A minimal third-party binary module: discovery + retry, a report section with fresh data captured at crash time, and key/value data.
Build it like any GMod binary module (see Building) and require it after require("crashcapture"); call MyThing_Tick() from your own timer or hook so it runs on the game thread.

#include "crashcapture_api.h"
#include <stdio.h>

#ifdef _WIN32
#define MY_EXPORT extern "C" __declspec(dllexport)
#else
#define MY_EXPORT extern "C" __attribute__((visibility("default")))
#endif

struct lua_State;

static CCAPI* cc = nullptr;
static int g_ticks = 0;
static int g_offenders = 0;

static void MyThingSection(void* user, CCSectionCtx* w)
{
    w->format(w, "ticks=%d offenders=%d\n", g_ticks, g_offenders);
}

static bool EnsureApi()
{
    if (cc) return true;
    cc = CC_ENSURE_API();
    if (!cc) return false;
    cc->add_section("mything", MyThingSection, nullptr);
    cc->set_data("mything.ticks", "0");
    return true;
}

MY_EXPORT int gmod13_open(struct lua_State* L)
{
    (void)L;
    EnsureApi(); // crashcapture may not be loaded yet; retry via MyThing_Tick
    return 0;
}

MY_EXPORT int gmod13_close(struct lua_State* L)
{
    (void)L;
    if (cc) {
        cc->remove_section("mything");
        cc->clear_data("mything.ticks");
        cc = nullptr;
    }
    return 0;
}

MY_EXPORT void MyThing_Tick()
{
    g_ticks++;
    if (!EnsureApi()) return;
    if (cc->is_hung()) {
        // the watchdog is already on it, this tick will be reported
    }
}

Versioning

CCAPI starts with size and version.
New functions are appended, so callers should validate with CC_API_OK(api, minSize) using the size of the trailing subset they use.
That keeps a module built against a newer header working with an older plugin, as long as it only relies on the fields it sized for.

Clone this wiki locally