C Script User Guide
C Script lets you write C-like scripts that are compiled to bytecode by the interpreter and executed in a virtual machine in Beacon. It is intended for small, practical operator tasks that require Windows API access and less noisy execution than BOFs.
If you already know C, the day-to-day syntax should feel familiar. The main differences are the execution model, the way native functions are declared, and the fact that the interpreter supports a focused subset of C rather than every feature of a full native C compiler and preprocessor.
The following guide walks through C Script examples and introduces how to develop working scripts.
Execution Model
To start, a script does not need a main function. Top-level statements are the entry point and run from top to bottom. Function bodies are compiled when they are defined, but they run only when called.
#include <beacon.h>
void say_hello(void)
{
BeaconPrintf(CALLBACK_OUTPUT, "hello from a helper");
}
BeaconPrintf(CALLBACK_OUTPUT, "script started");
say_hello();
BeaconPrintf(CALLBACK_OUTPUT, "script finished");
You can still define main if that makes ported code easier to organize. Call it explicitly from the top level when you want it to run.
#include <beacon.h>
int main(void)
{
BeaconPrintf(CALLBACK_OUTPUT, "inside main");
return 0;
}
main();
Include Files
Use the provided interpreter headers for common Windows and Beacon definitions. For a list of available headers, data structures, and function definitions, see the C Script headers GitHub repository. Clone the GitHub repository if you want to search the headers locally or provide them as reference material for AI-assisted scripting. The repository also includes a skills.md file and a quick demo that can help AI tools generate C Script examples.
When you include a header file in one of your scripts, the script compiler looks up the associated header in its local header folder. This folder is in the root directory where the Team Server runs. By default, the folder is called interpreter. The folder does not exist until you run your first script. The compiler first checks whether the headers are available and extracts them if they are not.
Inside the interpreter folder, the interpreter/includes folder contains two subfolders: system and user. The system folder contains the primary headers that are available from our GitHub repository. These headers are subject to change between releases and should not be edited because they can be overwritten. The user folder is empty by default and is where you can store custom headers that you want to reference in your scripts. Header files in this folder are not overwritten.
The most common starting points are:
-
#include <beacon.h>for Beacon APIs such asBeaconPrintf,BeaconDataParse,BeaconDataExtract, andBeaconFormat*. -
#include <windows.h>for common Windows types and API declarations. -
#include <msvcrt.h>for common C runtime declarations such asprintf,strlen,memcpy, and related functions.
#include <windows.h>
#include <beacon.h>
DWORD pid;
pid = GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "pid: %lu", pid);
Header coverage is intentionally practical rather than a complete copy of the Windows SDK. Before adding your own type, structure, constant, or native function declaration, check whether a provided header already defines it. If compilation reports a duplicate definition, remove the script-local definition and use the header version.
When a script must select 32-bit or Unicode aliases from headers, define the selection macros before any includes (the default is 64-bit and ANSI):
#define _WIN32
#define UNICODE
#include <windows.h>
Basic C Syntax
C Script supports common C control flow, integer expressions, functions, arrays, pointers, structures, unions, enums, typedefs, casts, and many ordinary operators.
#include <beacon.h>
int sum_values(int *values, int count)
{
int i;
int total;
total = 0;
for (i = 0; i < count; i++) {
total += values[i];
}
return total;
}
int values[4] = { 2, 4, 6, 8 };
BeaconPrintf(CALLBACK_OUTPUT, "sum: %d", sum_values(values, 4));
Supported control flow includes if, else, for, while, do/while, switch, case, default, break, continue, labels, and goto.
#include <beacon.h>
int status = 2;
switch (status) {
case 0:
BeaconPrintf(CALLBACK_OUTPUT, "ready");
break;
case 1:
case 2:
BeaconPrintf(CALLBACK_OUTPUT, "busy");
break;
default:
BeaconPrintf(CALLBACK_ERROR, "unknown status");
break;
}
Types
Use normal C integer and pointer types in script code:
-
char,short,int,long, andlong long -
signed and unsigned variants
-
pointers such as
char *,void *, andDWORD * -
arrays
-
structures, unions, enums, and typedefs
-
size_tand common Windows typedefs from the provided headers
Floating-point types such as float and double are not supported. Use integer or fixed-point math when fractional values are required.
For API-facing declarations, prefer explicit Windows typedefs or fixed-size native signature atoms, depending on where the type is used. For ordinary C code, use the C or Windows type names. For native foreign function interface (FFI) signatures, use only the atom types described in the Native Function Calls section.
Pointers and Arrays
Pointer arithmetic, arrays, pointer-to-pointer values, string literals, and structure field access work in ordinary C style.
#include <beacon.h>
char text[] = "abcdef";
char *p = text;
p += 2;
BeaconPrintf(CALLBACK_OUTPUT, "%c %s", *p, p);
Cast void * to a concrete pointer type before indexing or dereferencing it.
#include <beacon.h>
int values[2] = { 10, 20 };
void *raw = values;
int *typed = (int *)raw;
BeaconPrintf(CALLBACK_OUTPUT, "first: %d", typed[0]);
Structures, Unions, Enums, and Typedefs
Structures, unions, enums, typedefs, nested fields, ., and -> access are supported for common scripting patterns.
#include <beacon.h>
typedef struct _ITEM {
int id;
char *name;
} ITEM;
ITEM item;
ITEM *ptr;
item.id = 7;
item.name = "example";
ptr = &item;
BeaconPrintf(CALLBACK_OUTPUT, "%d %s", ptr->id, ptr->name);
When passing complex data to helper functions, do not pass arrays or unions by value because this is not supported. Instead, pass a pointer to the structure.
#include <beacon.h>
typedef struct _COUNTER {
int value;
} COUNTER;
void increment(COUNTER *counter)
{
counter->value++;
}
COUNTER counter;
counter.value = 41;
increment(&counter);
BeaconPrintf(CALLBACK_OUTPUT, "value: %d", counter.value);
Macros
Simple object-like and function-like macros are supported and are useful for constants and expression helpers.
#include <beacon.h>
#define MAX_ITEMS 4
#define SQUARE(x) ((x) * (x))
int value = SQUARE(MAX_ITEMS);
BeaconPrintf(CALLBACK_OUTPUT, "value: %d", value);
Macro support is not intended to match every feature of a full standalone C preprocessor. Keep macros short and expression-oriented. Prefer normal helper functions for multi-statement logic or complicated type-dependent behavior.
Native Function Calls
Native Windows and C runtime calls are made through FFI declarations. The declaration gives the library name, function name, optional calling convention, return type, and argument types.
KERNEL32$GetCurrentProcessId: stdcall u32 ();
KERNEL32$OutputDebugStringA: stdcall void (cstr);
MSVCRT$printf: cdecl i32 (cstr, ...);
After a function is declared, you can call it with its full LIBRARY$Function name or, when the name is unambiguous, by the bare function name.
#include <msvcrt.h>
KERNEL32$GetCurrentProcessId: stdcall u32 ();
unsigned int pid;
pid = GetCurrentProcessId();
printf("pid: %u\n", pid);
The supported FFI signature atom types are:
-
void -
i16,u16 -
i32,u32 -
i64,u64 -
ptr -
cstr -
size_t
Use ptr for general pointers, cstr for narrow C strings, and size_t for pointer-sized size values. Non-variadic native calls must pass the number of arguments declared in the signature. Pointer parameters should receive pointer expressions or NULL, not arbitrary integer values.
Calling convention matters for 32-bit execution. Use cdecl for C runtime functions and variadic functions such as printf. Use stdcall for most Win32 API calls. Variadic native declarations are supported only with cdecl; do not declare stdcall varargs. On 64-bit Windows, the platform ABI is unified, but keeping the convention in declarations improves portability to 32-bit scripts.
Calling APIs from Headers
Most scripts should include headers instead of writing every native declaration manually.
#include <windows.h>
#include <beacon.h>
DWORD tick;
tick = GetTickCount();
BeaconPrintf(CALLBACK_OUTPUT, "tick: %lu", tick);
If a function or type is not available in the provided headers, declare only the missing pieces your script needs.
#include <beacon.h>
USER32$MessageBeep: stdcall i32 (u32);
if (MessageBeep(0xFFFFFFFF) == 0) {
BeaconPrintf(CALLBACK_ERROR, "MessageBeep failed");
}
Script Arguments
The interpreter provides two globals:
-
__argc: the size, in bytes, of the packed argument buffer. -
__argv: a pointer to the packed argument buffer.
These are not the traditional C argc and argv values from main(int argc, char **argv). Parse them with the Beacon data APIs.
#include <beacon.h>
if (__argc > 0) {
datap parser;
char *message;
BeaconDataParse(&parser, __argv, __argc);
message = BeaconDataExtract(&parser, NULL);
if (message != NULL) {
BeaconPrintf(CALLBACK_OUTPUT, "message: %s", message);
}
else {
BeaconPrintf(CALLBACK_ERROR, "missing message");
}
}
else {
BeaconPrintf(CALLBACK_ERROR, "no arguments supplied");
}
Use the matching extraction function for the way each argument was packed. Common helpers include BeaconDataExtract, BeaconDataInt, BeaconDataShort, BeaconDataLength, and BeaconDataPtr.
Producing Output
Use BeaconPrintf for short status messages and small results.
#include <beacon.h>
BeaconPrintf(CALLBACK_OUTPUT, "operation completed");
For larger output, build the result with BeaconFormat* and emit it in a controlled way instead of calling BeaconPrintf repeatedly in a tight loop.
#include <beacon.h>
formatp format;
char *buffer;
int length;
BeaconFormatAlloc(&format, 1024);
BeaconFormatPrintf(&format, "name: %s\n", "example");
BeaconFormatPrintf(&format, "count: %d\n", 3);
buffer = BeaconFormatToString(&format, &length);
if (buffer != NULL && length > 0) {
BeaconOutput(CALLBACK_OUTPUT, buffer, length);
}
BeaconFormatFree(&format);
Native Function Pointers
C Script supports practical native function-pointer patterns, including resolving a function address and calling it through a typed pointer.
#include <windows.h>
#include <msvcrt.h>
size_t (cdecl *my_strlen)(char *s);
void *module;
void *proc;
module = GetModuleHandleA("msvcrt.dll");
proc = GetProcAddress(module, "strlen");
if (proc != NULL) {
my_strlen = proc;
printf("length: %u\n", (unsigned int)my_strlen("hello"));
}
Keep function-pointer declarations simple. Script-defined functions are not a valid replacement for native callbacks; use function pointers for supported native FFI call patterns.
Debugging
To trace script execution, you can use debug logging. The internal.h header has a debugging function for this called DbgPrintf which routes debug output through an OutputDebugStringA call in the Beacon virtual machine. Because the interpreter does not support variadic in-script functions, this API provides formatted debug output for scripts. You can view debugging output with a tool such as DebugView from Sysinternals.
#include <internal.h>
DbgPrintf("Address 0x%X", 0xDEADBEEF);
Porting C Code
When adapting existing C or BOF-style code:
-
Start with the top-level script flow. Add
main();only if the existing code is organized around amainfunction. -
Include provided headers before copying local type or API declarations.
-
Replace unsupported floating-point logic with integer logic.
-
Pass structures by pointer.
-
Convert native imports to
LIBRARY$FunctionFFI declarations when the provided headers do not already include them. -
Parse runtime arguments through
BeaconDataParseinstead ofargcandargv. -
Keep macros and declarations conservative.
Limitations
C Script is designed for compact, reliable interpreter scripts. The compiler is not a full native C compiler. Important limitations include:
-
No
floatordoublesupport. -
No implicit main entry point.
-
__argcand__argvare packed Beacon argument data, not POSIX-style argument values. -
Native calls require FFI declarations or provided header declarations.
-
FFI signatures must use the supported atom types only.
-
Native varargs are supported only for cdecl declarations.
-
Script-defined varargs functions are not supported.
-
Complex function-pointer declarations and callback-style designs should be avoided unless they match known supported native-call patterns.
-
void * must be cast before dereference or indexing.
-
Passing or returning structures and unions by value is not supported; pass pointers instead.
-
Macro support is practical but not equivalent to a complete C preprocessor.
-
Header coverage is not a complete Windows SDK replacement.
-
Runtime resources such as call depth, operand stack space, locals, globals, and aggregate field counts are finite.
Compatibility Tips
Write scripts in a straightforward C89 style: declare variables at the start of blocks, prefer explicit casts, avoid complex declarators, and keep helper functions small. Use the provided headers, reuse existing definitions, and keep native signatures close to the Windows API documentation. These habits make scripts easier to read, easier to port, and more likely to behave consistently across 32-bit and 64-bit execution.