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
Co-authored-by: Andrew Kesterson <andrew@starfort.tech>
179 lines
7.7 KiB
C
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_
|