From e24186fc1917c5e8b91924af0c3c7c55816ff5d6 Mon Sep 17 00:00:00 2001 From: Bad Diode Date: Mon, 25 Jan 2021 22:01:00 +0100 Subject: Introducing MIC --- .gitignore | 1 + LICENSE | 51 +++++++++++++++ Makefile | 61 ++++++++++++++++++ README.md | 118 +++++++++++++++++++++++++++++++++++ src/app.c | 44 +++++++++++++ src/app.h | 37 +++++++++++ src/main.c | 25 ++++++++ src/platform.h | 25 ++++++++ src/platform_posix.c | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/shorthand.h | 35 +++++++++++ 10 files changed, 568 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/app.c create mode 100644 src/app.h create mode 100644 src/main.c create mode 100644 src/platform.h create mode 100644 src/platform_posix.c create mode 100644 src/shorthand.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa5bd70 --- /dev/null +++ b/LICENSE @@ -0,0 +1,51 @@ +This software is dual licensed under Unlicense/MIT. Choose whichever you prefer. + +---------------------------------------------------------------------- +UNLICENSE +---------------------------------------------------------------------- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + + +---------------------------------------------------------------------- +MIT License +---------------------------------------------------------------------- + +Copyright (c) 2020 Bad Diode + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b31e5c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +.POSIX: +.SUFFIXES: + +# Source code location and files to watch for changes. +SRC_DIR := src +SRC_MAIN := $(SRC_DIR)/main.c +SRC_APP := $(SRC_DIR)/app.c +WATCH_SRC := $(wildcard $(SRC_DIR)/*.c) +WATCH_SRC += $(wildcard $(SRC_DIR)/*.h) + +# Output library names and executables. +BIN_NAME := app +LIB_NAME := libmic.so +BUILD_DIR := build +BIN := $(BUILD_DIR)/$(BIN_NAME) +LIB := $(BUILD_DIR)/$(LIB_NAME) +LIB_DIR := $(BUILD_DIR) + +# Compiler and linker configuration. +CC := gcc +CFLAGS := -Wall -Wextra -pedantic -std=c99 +LDFLAGS := +LDLIBS := +RELEASE_CFLAGS := -DNDEBUG -O2 +DEBUG_CFLAGS := -DDEBUG -g + +.PHONY: dynamic static clean run + +# Setup debug/release builds. +# make clean && make DEBUG=0 +# make clean && make DEBUG=1 +DEBUG ?= 0 +ifeq ($(DEBUG), 1) + CFLAGS += $(DEBUG_CFLAGS) +else + CFLAGS += $(RELEASE_CFLAGS) +endif + +dynamic: CFLAGS += -fPIC +dynamic: CFLAGS += -DLIB_NAME=\"$(LIB_NAME)\" +dynamic: CFLAGS += -DLIB_DIR=\"$(LIB_DIR)\" +dynamic: LDFLAGS += -ldl +dynamic: $(BUILD_DIR) $(LIB) $(BIN) + +static: SRC_MAIN += $(SRC_APP) +static: $(BUILD_DIR) $(BIN) + +$(BIN): $(SRC_MAIN) $(WATCH_SRC) + $(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN) $(SRC_MAIN) $(LDLIBS) + +$(LIB): $(SRC_APP) $(WATCH_SRC) + $(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ $(SRC_APP) $(LDLIBS) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +run: $(BIN) + exec $(BIN) + +clean: + rm -r $(BUILD_DIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..56d8b9d --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# Micro Interactive C Framework (MIC) + +MIC is a tiny (<300 cloc) framework to boostrap interactive C projects. +Application code can be dynamically reloaded upon compilation, allowing C to be +used as a pseudo-scripting language. This property can be very useful when +developing, but might not be always desirable for a final release, for this +reason, static compilation can be selected as a make target when needed. The +main limitation of hot code reload is that global state can't be stored in the +main app code, but the framework is designed so the state is passed around when +needed and is always accessible for the relevant functions. + +This framework can be easily extended to work with multiple platforms. However, +hot code reload will only be available for platforms that support dynamic +linking. For the moment, only POSIX compliant platforms are supported but +Windows is a target for the near future. + +Remember that MIC is only a base, you can (and should) modify anything in here +to adapt it to your particular project. + +## Example usage + +Compile and execute the program: + +``` +make && ./build/app +``` + +While the application is running, make changes to the `src/app.c`. For example, +change the `app_step` function from: + +``` +static inline bool +app_step(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("STEP"); + platform.sleep(100000); + return true; +} +``` + +To: + +``` +static inline bool +app_step(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("Hello world!"); + platform.sleep(100000); + return true; +} +``` + +In another terminal (or using your editor of choice), recompile the program: + +``` +make +``` + +The changes should take effect in the running application. + +## Implementation details + +This framework offers two APIs for managing system resources and executing +application logic. The `PlatformAPI` is described in `src/platform.h` and it's +tasked with manage the resources of the platform and/or operating system. For +example, opening/reading files, allocating memory, logging, etc. To support +a new platform it is sufficient to implement the functions described on the API +and make sure to select such platform at compile time. Additionally, the +following internal functions must be implemented for static and dynamic linking: + +``` +// Load the dynamic library and initialize application. Returns the success or +// failure of this operation. +static bool _app_init(AppAPI *api, AppState *state, PlatformAPI platform); + +// This function reloads the application code from the dynamic library, +// returning `true` if the AppAPI is ready to be used. +static bool _app_reload(); + +// Cleanup resources before exit. +static void _app_destroy(AppAPI *api, AppState *state, PlatformAPI platform); +``` + +The `AppAPI`, described in `src/app.h` and handles app initialization and +destruction, it contains the behaviour in the event of hot code reloading as +well as the main step function. The `init` function typically will be tasked +with initial allocation of resources. The duty of `reload` and `unload` is +application dependent, for example, we might wish to re-compile the shader +programs or reload geometry/sprites when we make modifications to the main +application code. The `step` function is called in every iteration of the loop, +and is where the bulk of the logic will likely be. + +The application state is stored in a unique `AppState` structure. MIC has been +architected so that the state and platform functions are available in the +previously mentioned `AppAPI` functions. `AppState` can contain whatever state +is needed and will be persistent when re-compiling. The only limitation is that +if the `AppState` structure changes, the application must be restarted, as the +stored state will become invalid. This can be circumvented by pre-allocating +some memory that can then be partitioned with a pool allocator to suit the +application needs. This design is also conducive for saving state data by simply +serializing the `AppState` structure. + +## Credits + +I initially discovered the hot code reloading trick when watching [Casey +Muratori's][casey] [Handmade Hero][handmade] series. It blew my mind! However, +the code he presented was focused solely on Windows and I never end up trying it +myself. Some years later I tried to find a way of making this work in Mac/Linux +when I came across [Chris Wellons'][skeeto] take on this problem with a clever +function pointer API in his [interactive c demo][interac-c-demo] page. This +framework uses a lot of the ideas he presents there, as you can clearly see if +you look at [his code][interac-c-demo]. + +[casey]: https://caseymuratori.com +[handmade]: https://handmadehero.org +[skeeto]: https://nullprogram.com +[interac-c-demo]: https://nullprogram.com/blog/2014/12/23/ +[interac-c-demo-github]: https://github.com/skeeto/interactive-c-demo diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..a39bff0 --- /dev/null +++ b/src/app.c @@ -0,0 +1,44 @@ +#include "app.h" +#include "platform.h" + +static inline bool +app_init(AppState *state, PlatformAPI platform) { + platform.log("INIT"); + state->lt_memory = platform.calloc(LT_MEMORY_SIZE, sizeof(u8)); + state->st_memory = platform.calloc(ST_MEMORY_SIZE, sizeof(u8)); + return true; +} + +static inline void +app_destroy(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("DESTROY"); +} + +static inline void +app_reload(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("RELOAD"); +} + +static inline void +app_unload(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("UNLOAD"); +} + +static inline bool +app_step(AppState *state, PlatformAPI platform) { + (void)state; // Unused parameter. + platform.log("STEP"); + platform.sleep(100000); + return true; +} + +const AppAPI APP_API = { + .init = app_init, + .destroy = app_destroy, + .reload = app_reload, + .step = app_step, + .unload = app_unload, +}; diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..8edbe58 --- /dev/null +++ b/src/app.h @@ -0,0 +1,37 @@ +#ifndef MIC_APP_H +#define MIC_APP_H + +#include "shorthand.h" +#include "platform.h" + +#define LT_MEMORY_SIZE GB(2) +#define ST_MEMORY_SIZE MB(100) + +typedef struct AppState { + // Long and short term memory. + char *lt_memory; + char *st_memory; +} AppState; + +// Function pointers for the AppAPI. +typedef struct AppAPI { + // Initialization code. Meant to be called exactly once before other API + // interactions. Returns the success or failure of the initialization + // process. + bool (*init)(AppState *state, PlatformAPI platform); + + // Resource deallocation and cleanup. Meant to be called exactly once at the + // end of the app. + void (*destroy)(AppState *state, PlatformAPI platform); + + // In case of hot code reloading, these functions handle the necessary + // resource setup and reloading. + void (*reload)(AppState *state, PlatformAPI platform); + void (*unload)(AppState *state, PlatformAPI platform); + + // Main update/step function for the app. Returns if the app should keep + // running. + bool (*step)(AppState *state, PlatformAPI platform); +} AppAPI; + +#endif // MIC_APP_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..52728ef --- /dev/null +++ b/src/main.c @@ -0,0 +1,25 @@ +#include "platform_posix.c" + +int +main(void) { + // App initialization. + AppAPI api = {0}; + AppState state = {0}; + if (!_app_init(&api, &state, PLATFORM_API)) { + return EXIT_FAILURE; + } + + // Main loop. + for (;;) { + if (!_app_reload(&api, &state, PLATFORM_API)) { + continue; + } + if (!api.step(&state, PLATFORM_API)) { + break; + } + } + + // Cleanup. + _app_destroy(&api, &state, PLATFORM_API); + return EXIT_SUCCESS; +} diff --git a/src/platform.h b/src/platform.h new file mode 100644 index 0000000..877453c --- /dev/null +++ b/src/platform.h @@ -0,0 +1,25 @@ +#ifndef MIC_PLATFORM_H +#define MIC_PLATFORM_H + +// Function pointers for the PlatformAPI. This allows the app to call platform +// specific functions that perform IO, memory allocations, etc. +typedef struct PlatformAPI { + // Reads an entire file into a null terminated buffer. It doesn't perform + // memory allocations and may crash if there is not enough memory or if it + // is uninitialized. Returns the number of bytes read. + size_t (*read_file)(const char *path, char *memory); + + // Custom memory allocation functions for the platform. + void *(*malloc)(size_t size); + void (*free)(void *ptr); + void *(*calloc)(size_t nmemb, size_t size); + void *(*realloc)(void *ptr, size_t size); + + // Sleep/wait for a given number of microseconds. + void (*sleep)(size_t microseconds); + + // Logging functions. + void (*log)(const char *format, ...); +} PlatformAPI; + +#endif // MIC_PLATFORM_H diff --git a/src/platform_posix.c b/src/platform_posix.c new file mode 100644 index 0000000..7c2fb6c --- /dev/null +++ b/src/platform_posix.c @@ -0,0 +1,171 @@ +#define _DEFAULT_SOURCE +#include +#include +#include +#include +#include + +#include "app.h" + +// +// PlatformAPI Implementation. +// + +size_t +platform_read_file(const char *path, char *memory) { + size_t file_size = 0; + FILE *fp = fopen(path, "rb"); + if (!fp) { + return 0; + } + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + rewind(fp); + fread(memory, 1, file_size, fp); + fclose(fp); + memory[file_size] = '\0'; + return file_size; +} + +void +platform_log(const char *format, ...) { + // Print date. + time_t raw_time = time(NULL); + struct tm *tm = localtime(&raw_time); + char date[64]; + strftime(date, sizeof(date), "%Y-%M-%d | %H:%M:%S", tm); + printf("%s | ", date); + + // Print message. + va_list args; + va_start(args, format); + vprintf(format, args); + + printf("\n"); + va_end(args); +} + +void +platform_sleep(size_t microseconds) { + usleep(microseconds); +} + +#if defined(LIB_NAME) && defined(LIB_DIR) + +// +// Dynamic linking with hot code reload. +// + +#include +#include +#include + +static ino_t lib_id; +static void * lib_handle; + +static const char *lib_path = LIB_DIR "/" LIB_NAME; + +static bool +load_lib(AppAPI *api) { + struct stat st; + if (stat(lib_path, &st) != 0) { + return false; + } + + if (lib_id != st.st_ino) { + void *handle = dlopen(lib_path, RTLD_NOW); + if (!handle) { + lib_handle = NULL; + lib_id = 0; + return false; + } + lib_handle = handle; + lib_id = st.st_ino; + + const AppAPI *app_api = dlsym(lib_handle, "APP_API"); + if (app_api == NULL) { + dlclose(lib_handle); + lib_handle = NULL; + lib_id = 0; + return false; + } + *api = *app_api; + } + + return true; +} + +static bool +_app_reload(AppAPI *api, AppState *state, PlatformAPI platform) { + struct stat st; + if (stat(lib_path, &st) == 0 && lib_id != st.st_ino) { + if (lib_handle) { + api->unload(state, platform); + dlclose(lib_handle); + } + + if (!load_lib(api)) { + return false; + } + api->reload(state, platform); + } + + return true; +} + +static bool +_app_init(AppAPI *api, AppState *state, PlatformAPI platform) { + if (!load_lib(api)) { + fprintf(stderr, "error: can't open app library file: %s\n", lib_path); + return false; + } + api->init(state, platform); + return true; +} + +static void +_app_destroy(AppAPI *api, AppState *state, PlatformAPI platform) { + api->destroy(state, platform); + if (lib_handle) { + dlclose(lib_handle); + lib_handle = NULL; + lib_id = 0; + } +} + +#else + +// +// Static linking of app code. +// + +extern const AppAPI APP_API; + +static bool +_app_reload() { + return true; +} + +static bool +_app_init(AppAPI *api, AppState *state, PlatformAPI platform) { + *api = APP_API; + api->init(state, platform); + return true; +} + +static void +_app_destroy(AppAPI *api, AppState *state, PlatformAPI platform) { + api->destroy(state, platform); +} + +#endif + +const PlatformAPI PLATFORM_API = { + .read_file = platform_read_file, + .malloc = malloc, + .free = free, + .calloc = calloc, + .realloc = realloc, + .log = platform_log, + .sleep = platform_sleep, +}; diff --git a/src/shorthand.h b/src/shorthand.h new file mode 100644 index 0000000..9c2e2f0 --- /dev/null +++ b/src/shorthand.h @@ -0,0 +1,35 @@ +#ifndef MIC_SHORTHAND_H +#define MIC_SHORTHAND_H + +#include +#include +#include +#include + +// +// This simple header just typedefs the basic C define types to a shorter name, +// loads the quality of life bool macro for _Bool and defines shorthand macros +// for byte sizes. We need that the targeted architecture uses the floating +// point representation as described on the IEEE-754 standard. +// + +_Static_assert(sizeof(double) == 8, "no support for IEEE-754"); +_Static_assert(sizeof(float) == 4, "no support for IEEE-754"); + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; +typedef float f32; +typedef double f64; + +#define KB(N) ((u64)(N) * 1024) +#define MB(N) ((u64)KB(N) * 1024) +#define GB(N) ((u64)MB(N) * 1024) +#define TB(N) ((u64)GB(N) * 1024) + +#endif // MIC_SHORTHAND_H -- cgit v1.2.1