Files
akbasic/include/akbasic/host.h
Logikoma 21f360ed1d
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m43s
akbasic CI Build / sanitizers (push) Successful in 5m6s
akbasic CI Build / coverage (push) Successful in 4m14s
akbasic CI Build / akgl_build (push) Successful in 8m11s
akbasic CI Build / mutation_test (push) Successful in 23m0s
Reject overlong host type and field names
Co-authored-by: Andrew Kesterson <andrew@starfort.tech>
2026-08-06 12:09:08 -04:00

179 lines
7.7 KiB
C

/**
* @file host.h
* @brief Sharing a host program's own C structures with a script.
*
* A BASIC `TYPE` and a host C struct are the same thing seen from two sides, so
* both live in one type table and everything the language already does --
* copy on assign, `PTR TO`, `.` and `->` -- works across the boundary with no
* second set of rules.
*
* The host describes its struct once as a table of field descriptors and binds
* an instance to a name. A script then reads and writes the game's real memory:
*
* ```
* FOE@.HP# = FOE@.HP# - 10
* ```
*
* decrements the host's `hp` in place, with no marshalling step the host has to
* remember to run.
*
* **How the sharing works.** A binding takes a shadow run of value slots from
* the same pool a `DIM`med record uses, and every field read refreshes its slot
* from host memory while every write converts back and stores. So the script
* always sees current values and its writes always land, without the
* interpreter having to hold two storage models for one language.
*
* **Conversion refuses rather than truncates.** Writing 70000 into an `int16_t`
* field or 40 characters into a `char[32]` is an error naming the field, not a
* silent wrap. Two conversions are lossy and say so in the documentation: a host
* `float` round-trips through BASIC's doubles, and a `CSTRING` has a width BASIC
* strings do not.
*
* **This is the one place akbasic holds a pointer it did not allocate.**
* Everything else is pool-bounded and diagnosable. See akbasic_host_bind().
*/
#ifndef _AKBASIC_HOST_H_
#define _AKBASIC_HOST_H_
#include <stddef.h>
#include <akerror.h>
#include <akbasic/types.h>
struct akbasic_Runtime;
/** @brief How a host field is represented in C, which is what says how to convert it. */
typedef enum
{
AKBASIC_HOSTFIELD_INT8 = 0,
AKBASIC_HOSTFIELD_INT16,
AKBASIC_HOSTFIELD_INT32,
AKBASIC_HOSTFIELD_INT64,
AKBASIC_HOSTFIELD_UINT8,
AKBASIC_HOSTFIELD_UINT16,
AKBASIC_HOSTFIELD_UINT32,
AKBASIC_HOSTFIELD_BOOL,
AKBASIC_HOSTFIELD_FLOAT, /* C float; BASIC floats are double */
AKBASIC_HOSTFIELD_DOUBLE,
AKBASIC_HOSTFIELD_CSTRING, /* fixed char[]; `width` is its size */
AKBASIC_HOSTFIELD_STRUCT /* nested host struct; `typename_` names it */
} akbasic_HostFieldKind;
/** @brief One field of a host structure, in BASIC terms and in C terms. */
typedef struct
{
const char *name; /** BASIC field name, suffix included */
akbasic_HostFieldKind kind;
size_t offset; /** offsetof() within the host struct */
size_t width; /** sizeof() the member; CSTRING needs it */
const char *typename_; /** for STRUCT, the registered type name */
} akbasic_HostField;
/** @brief A host structure type: a name, a size, and its fields. */
typedef struct
{
const char *name; /** BASIC type name, no suffix */
size_t size; /** sizeof() the host struct */
const akbasic_HostField *fields;
int fieldcount;
} akbasic_HostType;
/**
* @brief Build a field descriptor from the member itself.
*
* Takes the offset and the width from the same member, so the two cannot drift
* apart -- writing them out by hand is two chances to name the wrong member and
* no way to notice. That is the only reason this is a macro.
*/
#define AKBASIC_HOST_FIELD(__struct, __member, __name, __kind) \
{ (__name), (__kind), offsetof(__struct, __member), \
sizeof(((__struct *)0)->__member), NULL }
/** @brief The same, for a field that is itself a registered host structure. */
#define AKBASIC_HOST_FIELD_STRUCT(__struct, __member, __name, __type) \
{ (__name), AKBASIC_HOSTFIELD_STRUCT, offsetof(__struct, __member), \
sizeof(((__struct *)0)->__member), (__type) }
/**
* @brief Register a host structure type under its BASIC name.
*
* The descriptor is **borrowed, not copied**: it must outlive the runtime, which
* is why every example makes it `static const`.
*
* Registering before the script is loaded is the normal case. A host type and a
* `TYPE` the script declares share one namespace, so a script cannot declare a
* type the host already registered -- and would be refused if it tried.
* Host type and field names are limited to 31 characters, matching
* script-declared types and fields.
*
* @param obj Object to initialize, inspect, or modify.
* @param type The host's description of its own struct.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either argument is NULL.
* @throws AKBASIC_ERR_VALUE When a field name carries no type suffix, a nested
* type is not registered, or the name is already taken.
* @throws AKERR_OUTOFBOUNDS When a type or field name exceeds 31 characters.
* @throws AKBASIC_ERR_BOUNDS When the type table or a field list is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_register_type(struct akbasic_Runtime *obj, const akbasic_HostType *type);
/**
* @brief Bind one instance of a registered host type to a script variable.
*
* Creates the variable in the script's *outermost* scope, for the reason
* akbasic_runtime_global() exists: a binding made during a suspended run would
* otherwise land in whatever `FOR` or `GOSUB` body happens to be active and die
* with it.
*
* **`instance` is borrowed and never copied.** It must outlive the binding. A
* struct on a stack frame that returns, or one the host frees, leaves the script
* reading freed memory -- and this is the only way to get a wild pointer into an
* interpreter that otherwise cannot produce one. Bind globals or arena objects,
* and call akbasic_host_unbind() before the storage goes.
*
* @param obj Object to initialize, inspect, or modify.
* @param name Script variable name, `@`-suffixed.
* @param typename_ A type registered by akbasic_host_register_type().
* @param instance The host's struct. Borrowed; must outlive the binding.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
* @throws AKBASIC_ERR_UNDEFINED When `typename_` is not registered.
* @throws AKBASIC_ERR_VALUE When `name` does not end in `@`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_bind(struct akbasic_Runtime *obj, const char *name, const char *typename_, void *instance);
/**
* @brief Point an existing binding at a different instance of the same type.
*
* The per-frame call. A host iterating its enemies rebinds one name rather than
* creating eight, which keeps the binding flat and lets one script be written
* against `FOE@` and run once per enemy.
*
* @param obj Object to initialize, inspect, or modify.
* @param name A name already bound by akbasic_host_bind().
* @param instance The host's struct. Borrowed; must outlive the binding.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any argument is NULL.
* @throws AKBASIC_ERR_UNDEFINED When `name` is not bound.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_rebind(struct akbasic_Runtime *obj, const char *name, void *instance);
/**
* @brief Break a binding, leaving the name pointing at nothing.
*
* Call it before the instance's storage goes away. After this a script reading
* that name is refused by name rather than reading freed memory, which is the
* difference between a diagnosable script error and undefined behaviour.
*
* @param obj Object to initialize, inspect, or modify.
* @param name A name already bound by akbasic_host_bind().
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either argument is NULL.
* @throws AKBASIC_ERR_UNDEFINED When `name` is not bound.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_host_unbind(struct akbasic_Runtime *obj, const char *name);
#endif // _AKBASIC_HOST_H_