/** * @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 #include #include 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. * * @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 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_