neonucleus/rewrite/neonucleus.h
2026-02-01 16:06:47 +01:00

442 lines
19 KiB
C

#ifndef NEONUCLEUS_H
#define NEONUCLEUS_H
#ifdef __cplusplus
extern "C" {
#endif
// every C standard header we depend on, conveniently put here
#include <stddef.h> // for NULL,
#include <stdint.h> // for intptr_t
#include <stdbool.h> // for true, false and bool
// Internally we need stdatomic.h and, if NN_BAREMETAL is not defined, stdlib.h and time.h
// The entire NeoNucleus API, in one header file
// Internal limits or constants
// the maximum amount of items the callstack can have.
#define NN_MAX_STACK 256
// the maximum size a path is allowed to have
#define NN_MAX_PATH 256
// the maximum amount of bytes which can be read from a file.
// You are given a buffer you are meant to fill at least partially, this is simply the limit of that buffer's size.
#define NN_MAX_READ 65536
// maximum size of a wakeup message
#define NN_MAX_WAKEUPMSG 2048
// the maximum amount of file descriptors that can be open simultaneously
#define NN_MAX_OPENFILES 128
// the maximum amount of userdata that can be sent simultaneously.
#define NN_MAX_USERDATA 64
// maximum size of a signal, computed the same as modem packet costs.
#define NN_MAX_SIGNALSIZE 8192
// maximum amount of signals.
#define NN_MAX_SIGNALS 128
// the maximum value of a port. Ports start at 1.
#define NN_MAX_PORT 65535
// the magic port number to close all ports
#define NN_CLOSEPORTS 0
// maximum amount of architectures one machine can support.
#define NN_MAX_ARCHITECTURES 32
// maximum amount of keyboards a screen can have
#define NN_MAX_KEYBOARDS 64
// the port used by tunnel cards. This port is invalid for modems.
#define NN_TUNNEL_PORT 0
// the maximum size of a UTF-8 character
#define NN_MAXIMUM_UNICODE_BUFFER 4
// the maximum size of a component error message. If the error is bigger than this,
// it is truncated.
#define NN_MAX_ERROR_SIZE 1024
// The type of a the function used as the allocator.
// The expected behavior is as follows:
// alloc(state, NULL, 0, newSize) -> malloc(newSize)
// alloc(state, memory, oldSize, 0) -> free(memory)
// alloc(state, memory, oldSize, newSize) -> realloc(memory, newSize)
//
// NeoNucleus will ensure oldSize is what the application requested on the last allocation.
// This is useful for allocators which may not do extensive bookkeeping.
// In the case of Out Of Memory, you are expected to return NULL.
typedef void *nn_AllocProc(void *state, void *memory, size_t oldSize, size_t newSize);
// Meant to return the time, in seconds, since some epoch.
typedef double nn_TimeProc(void *state);
typedef size_t nn_RngProc(void *state);
typedef enum nn_LockAction {
// init any necessary state
NN_LOCK_INIT,
// cleanup any necessary state
NN_LOCK_DEINIT,
// lock the mutex
NN_LOCK_LOCK,
// unlock the mutex
NN_LOCK_UNLOCK,
} nn_LockAction;
// intended for a plain mutex.
// This is used for synchronization. OpenComputers achieves synchronization
// between the worker threads by sending them as requests to a central thread (indirect methods).
// In NeoNucleus, we simply use a lock. This technically makes all methods direct, however
// we consider methods to be indirect if they require locks.
typedef void nn_LockProc(void *state, void *lock, nn_LockAction action);
// The *context* NeoNucleus is operating in.
// This determines:
// - How memory is allocated
// - How random numbers are generated
// - What the current time is
typedef struct nn_Context {
void *state;
nn_AllocProc *alloc;
nn_TimeProc *time;
// the maximum value the RNG can produce.
// The RNG is assumed to generate [0, rngMaximum]. INCLUSIVE.
// When a [0, 1) range is needed, the value is divided by (rngMaximum+1),
// so rngMaximum+1 MUST NOT OVERFLOW.
size_t rngMaximum;
nn_RngProc *rng;
// the size of 1 lock. This is used to dynamically allocate enough space for the locks.
size_t lockSize;
nn_LockProc *lock;
} nn_Context;
// if NN_BAREMETAL is defined when NeoNucleus is compiled,
// this function will fill in a bogus context that does nothing.
// Otherwise, it fills in a context which uses the C standard library.
// This function is only meant to be called once, to get the initial context.
// It does seed the RNG when using the C standard library, so do not
// call it in loops.
void nn_initContext(nn_Context *ctx);
typedef enum nn_Exit {
// no error
NN_OK = 0,
// out of memory.
NN_ENOMEM,
// over the limit. For example, adding too many architectures to a machine.
NN_ELIMIT,
// internal stack underflow when managing the stack.
NN_EBELOWSTACK,
// internal stack overflow when carrying values.
NN_ENOSTACK,
// bad invocation, error message stored in computer state
NN_EBADCALL,
} nn_Exit;
// This stores necessary data between computers
typedef struct nn_Universe nn_Universe;
nn_Universe *nn_createUniverse(nn_Context *ctx);
void nn_destroyUniverse(nn_Universe *universe);
// The actual computer
typedef struct nn_Computer nn_Computer;
typedef enum nn_ComputerState {
// the machine is running just fine
NN_RUNNING = 0,
// machine is initializing. This is the state after createComputer but before the first nn_tick.
// It is a required state of various initialization functions.
NN_BOOTUP,
// the machine has powered off.
NN_POWEROFF,
// the machine demands being restarted.
NN_RESTART,
// the machine has crashed. RIP
NN_CRASHED,
// the machine ran out of energy.
NN_BLACKOUT,
// change architecture.
NN_CHARCH,
} nn_ComputerState;
typedef enum nn_ArchitectureAction {
// create the local state
NN_ARCH_INIT,
// destroy the local state
NN_ARCH_DEINIT,
// run 1 tick
NN_ARCH_TICK,
// get the free memory
NN_ARCH_FREEMEM,
} nn_ArchitectureAction;
typedef struct nn_ArchitectureRequest {
// the state pointer passed through
void *globalState;
// the computer which made the request
nn_Computer *computer;
// the local state bound to this computer.
// In NN_ARCH_INIT, this is NULL, and must be set to the new state, or an appropriate exit code returned.
void *localState;
// the action requested
nn_ArchitectureAction action;
union {
// in the case of NN_ARCH_FREEMEM, the free memory
size_t freeMemory;
};
} nn_ArchitectureRequest;
typedef nn_Exit nn_ArchitectureHandler(nn_ArchitectureRequest *req);
typedef struct nn_Architecture {
const char *name;
void *state;
nn_ArchitectureHandler *handler;
} nn_Architecture;
// The state of a *RUNNING* computer.
// Powered off computers shall not have a state, and as far as NeoNucleus is aware,
// not exist.
// The computer API *is not thread-safe*, so it is recommended that you use an external lock to manage it if you are running
// it in a multi-threaded environment.
// The userdata pointer is meant to store external data required by the environment.
// totalMemory is a hint to the architecture as to how much memory to allow the runner to use. It is in bytes.
// maxComponents and maxDevices determine how many components can be connected simultaneously to this computer, and how much device info can be
// registered on this computer.
nn_Computer *nn_createComputer(nn_Universe *universe, void *userdata, const char *address, size_t totalMemory, size_t maxComponents, size_t maxDevices);
// Destroys the state, effectively shutting down the computer.
void nn_destroyComputer(nn_Computer *computer);
// get the userdata pointer
void *nn_getComputerUserdata(nn_Computer *computer);
const char *nn_getComputerAddress(nn_Computer *computer);
// Sets the computer's architecture.
// The architecture determines everything from how the computer runs, to how it turns off.
// Everything is limited by the architecture.
void nn_setArchitecture(nn_Computer *computer, const nn_Architecture *arch);
// Gets the current architecture.
nn_Architecture *nn_getArchitecture(nn_Computer *computer);
// Sets the computer's desired architecture.
// The desired architecture indicates, when the computer state is CHARCH, what the new architecture should be.
// This is set even if it is not in the supported architecture list, *you must check if it is in that list first.*
void nn_setDesiredArchitecture(nn_Computer *computer, const nn_Architecture *arch);
// Gets the desired architecture. This is the architecture the computer should use after changing architectures.
nn_Architecture *nn_getDesiredArchitecture(nn_Computer *computer);
// Adds a new supported architecture, which indicates to the code running on this computer that it is possible to switch to that architecture.
void nn_addSupportedArchitecture(nn_Computer *computer, const nn_Architecture *arch);
// Returns the array of supported architectures, as well as the length.
const nn_Architecture *nn_getSupportedArchitecture(nn_Computer *computer, size_t *len);
// Helper function for searching for an architecture using a computer which supports it and the architecture name.
const nn_Architecture *nn_findSupportedArchitecture(nn_Computer *computer, const char *name);
// sets the energy capacity of the computer.
void nn_setTotalEnergy(nn_Computer *computer, double maxEnergy);
// gets the energy capacity of the computer
double nn_getTotalEnergy(nn_Computer *computer);
// sets the current amount of energy
void nn_setEnergy(nn_Computer *computer, double energy);
// gets the current amount of energy
double nn_getEnergy(nn_Computer *computer);
// Returns true if there is no more energy left, and a blackout has occured.
bool nn_removeEnergy(nn_Computer *computer, double energy);
size_t nn_getTotalMemory(nn_Computer *computer);
size_t nn_getFreeMemory(nn_Computer *computer);
// gets the current uptime of a computer. When the computer is not running, this value can be anything and loses all meaning.
double nn_getUptime(nn_Computer *computer);
// copies the string into the local error buffer. The error is NULL terminated, but also capped by NN_MAX_ERROR_SIZE
void nn_setError(nn_Computer *computer, const char *s);
// copies the string into the local error buffer. The error is capped by NN_MAX_ERROR_SIZE-1. The -1 is there because the NULL terminator is still inserted at the end.
// Do note that nn_getError() still returns a NULL-terminated string, thus NULL terminators in this error will lead to a shortened error.
void nn_setLError(nn_Computer *computer, const char *s, size_t len);
// Gets a pointer to the local error buffer. This is only meaningful when NN_EBADCALL is returned.
const char *nn_getError(nn_Computer *computer);
// clears the computer's error buffer, making nn_getError return a simple empty string.
void nn_clearError(nn_Computer *computer);
// sets the computer state to the desired state. This is meant for architectures to report things like reboots or shutdowns, DO NOT ABUSE THIS.
void nn_setComputerState(nn_Computer *computer, nn_ComputerState state);
// gets the current computer state
nn_ComputerState nn_getComputerState(nn_Computer *computer);
// runs a tick of the computer. Make sure to check the state returned!
nn_ComputerState nn_tick(nn_Computer *computer);
typedef struct nn_DeviceInfoEntry {
const char *name;
const char *value;
} nn_DeviceInfoEntry;
typedef struct nn_DeviceInfo {
const char *address;
const nn_DeviceInfoEntry *entries;
} nn_DeviceInfo;
// adds some device information to the computer. This can also be removed.
// Entries is terminated by a NULL name, and preferrably also NULL value.
// It is perfectly fine to free entries after the call, it is copied.
nn_Exit nn_addDeviceInfo(nn_Computer *computer, const char *address, const nn_DeviceInfoEntry entries[]);
// Removes info assicated with a device
void nn_removeDeviceInfo(nn_Computer *computer, const char *address);
// gets the device info array.
const nn_DeviceInfo *nn_getDeviceInfo(nn_Computer *computer, size_t *len);
typedef struct nn_ComponentMethod {
const char *name;
const char *docString;
bool direct;
} nn_ComponentMethod;
typedef struct nn_ComponentType nn_ComponentType;
typedef enum nn_ComponentAction {
// create the local state
NN_COMP_INIT,
// delete the local state
NN_COMP_DEINIT,
// perform a method call
NN_COMP_CALL,
} nn_ComponentAction;
typedef struct nn_ComponentRequest {
// the userdata of the component type. This may be an associated VM, for example.
void *typeUserdata;
// the userdata of the component, passed in addComponent. This may be an associated resource, for example.
void *compUserdata;
// the local state of the component. NN_COMP_INIT should initialize this pointer.
void *state;
nn_Computer *computer;
nn_ComponentAction action;
// for NN_COMP_CALL, it is the method called.
const char *methodCalled;
// for NN_COMP_CALL, it is the amount of return values.
size_t returnCount;
} nn_ComponentRequest;
typedef nn_Exit nn_ComponentHandler(nn_ComponentRequest *req);
// Creates a new component type. It is safe to free name and methods afterwards.
nn_ComponentType *nn_createComponentType(nn_Universe *universe, const char *name, void *userdata, const nn_ComponentMethod methods[], nn_ComponentHandler *handler);
void nn_destroyComponentType(nn_ComponentType *ctype);
// adds a component. Outside of the initialization state (aka after the first tick), it also emits the signal for component added.
// You MUST NOT destroy the component type while a component using that type still exists.
// You can free the address after the call just fine.
nn_Exit nn_addComponent(nn_Computer *computer, nn_ComponentType *ctype, const char *address, size_t slot, void *userdata);
// Checks if a component of that address exists.
bool nn_hasComponent(nn_Computer *computer, const char *address);
// removes a component. Outside of the initialization state (aka after the first tick), it also emits the signal for component removed.
nn_Exit nn_removeComponent(nn_Computer *computer, const char *address);
// Gets the name of a type of a component.
const char *nn_getComponentType(nn_Computer *computer, const char *address);
// Gets the slot of a component.
size_t nn_getComponentSlot(nn_Computer *computer, const char *address);
// Returns the array of component methods. This can be used for doc strings or just listing methods.
const nn_ComponentMethod *nn_getComponentMethods(nn_Computer *computer, const char *address, size_t *len);
// this uses the call stack.
// Component calls must not call other components, it just doesn't work.
// The lack of an argument count is because the entire call stack is assumed to be the arguments.
nn_Exit nn_call(nn_Computer *computer, const char *address, const char *method);
// call stack operations.
// The type system and API are inspired by Lua, as Lua remains the most popular architecture for OpenComputers.
// This does support other languages, however it may make some APIs clunky due to the usage of tables and 1-based indexing.
// Internally, reference counting is used to manage the memory automatically. The API is designed such that strong reference cycles
// cannot occur.
// pushes a null on the call stack
nn_Exit nn_pushnull(nn_Computer *computer);
// pushes a boolean on the call stack
nn_Exit nn_pushbool(nn_Computer *computer, bool truthy);
// pushes a number on the call stack
nn_Exit nn_pushnumber(nn_Computer *computer, double num);
// pushes a NULL-terminated string on the call stack. The string is copied, so you can free it afterwards without worry.
nn_Exit nn_pushstring(nn_Computer *computer, const char *str);
// pushes a string on the call stack. The string is copied, so you can free it afterwards without worry. The copy will have a NULL terminator inserted
// at the end for APIs which need it, but the length is also stored.
nn_Exit nn_pushlstring(nn_Computer *computer, const char *str, size_t len);
// pushes a computer userdata to the stack. This is indicative of a resource, such as an HTTP request.
nn_Exit nn_pushuserdata(nn_Computer *computer, size_t userdataIdx);
// pushes a table meant to be an array. [len] is the length of the array. The keys are numbers and 1-indexed, just like in Lua.
// The values are popped, then the array is pushed.
nn_Exit nn_pusharraytable(nn_Computer *computer, size_t len);
// pushes a table. [len] is the amount of pairs. Keys should not be duplicated, as they are not de-duplicated.
// The stack should have a sequence of K1,V1,K2,V2,etc., len pairs long.
// The pairs are popped, then the array is pushed.
nn_Exit nn_pushtable(nn_Computer *computer, size_t len);
// stack management
// pops the top value off the stack
nn_Exit nn_pop(nn_Computer *computer);
// pops the top N values off the stack
nn_Exit nn_popn(nn_Computer *computer, size_t n);
// pushes the top value onto the stack, effectively duplicating the top value.
nn_Exit nn_dupe(nn_Computer *computer);
// pushes the top N values onto the stack, effectively duplicating the top N values.
nn_Exit nn_dupen(nn_Computer *computer, size_t n);
// get the current amount of values on the call stack.
// For component calls, calling this at the start effectively gives you the argument count.
size_t nn_getstacksize(nn_Computer *computer);
// Removes all values from the stack.
void nn_clearstack(nn_Computer *computer);
// type check! The API may misbehave if types do not match, so always type-check!
// Returns whether the value at [idx] is a null.
// [idx] starts at 0.
bool nn_isnull(nn_Computer *computer, size_t idx);
// Returns whether the value at [idx] is a boolean.
// [idx] starts at 0.
bool nn_isboolean(nn_Computer *computer, size_t idx);
// Returns whether the value at [idx] is a number.
// [idx] starts at 0.
bool nn_isnumber(nn_Computer *computer, size_t idx);
// Returns whether the value at [idx] is a string.
// [idx] starts at 0.
bool nn_isstring(nn_Computer *computer, size_t idx);
// Returns whether the value at [idx] is a userdata.
// [idx] starts at 0.
bool nn_isuserdata(nn_Computer *computer, size_t idx);
// Returns whether the value at [idx] is a table.
// [idx] starts at 0.
bool nn_istable(nn_Computer *computer, size_t idx);
// NOTE: behavior of the nn_to*() functions when the types mismatch is undefined.
// Returns the boolean value at [idx].
bool nn_toboolean(nn_Computer *computer, size_t idx);
// Returns the number value at [idx].
double nn_tonumber(nn_Computer *computer, size_t idx);
// Returns the string value at [idx].
const char *nn_tostring(nn_Computer *computer, size_t idx);
// Returns the string value and its length at [idx].
const char *nn_tolstring(nn_Computer *computer, size_t idx, size_t *len);
// Returns the userdata index at [idx].
size_t nn_touserdata(nn_Computer *computer, size_t idx);
// Takes a table value and pushes onto the stack the key-value pairs, as well as writes how many there were in [len].
// It pushes them as K1,V1,K2,V2,K3,V3,etc., just like went in to pushtable. And yes, the keys are not de-duplicated.
nn_Exit nn_dumptable(nn_Computer *computer, size_t idx, size_t *len);
// Returns the amount of signals stored
size_t nn_countSignals(nn_Computer *computer);
// Pops [valueCount] values from the call stack and pushes them as a signal.
nn_Exit nn_pushSignal(nn_Computer *computer, size_t valueCount);
// Removes the first signal and pushes the values onto the call stack, while setting valueCount to the amount of values in the signal.
nn_Exit nn_popSignal(nn_Computer *computer, size_t *valueCount);
// The high-level API of the built-in components.
// These components still make no assumptions about the OS, and still require handlers to connect them to the outside work.
// Initializes the component library for a universe. This just defines the component tables and stores them in the universe.
// Using the built-in components without calling this will cause insane levels of undefined behavior and may even cause time travel
// and singularities forming inside your computer.
nn_Exit nn_initComponentsLibrary(nn_Universe *universe);
// TODO: screen, gpu, filesystem, eeprom and the rest of the universe
#ifdef __cplusplus
}
#endif
#endif