aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBad Diode <bd@badd10de.dev>2021-01-25 22:01:00 +0100
committerBad Diode <bd@badd10de.dev>2021-01-25 22:01:00 +0100
commite24186fc1917c5e8b91924af0c3c7c55816ff5d6 (patch)
tree6b20f24824736e822ba614811f312a7c7c2fd43b
downloadmic-e24186fc1917c5e8b91924af0c3c7c55816ff5d6.tar.gz
mic-e24186fc1917c5e8b91924af0c3c7c55816ff5d6.zip
Introducing MIC
-rw-r--r--.gitignore1
-rw-r--r--LICENSE51
-rw-r--r--Makefile61
-rw-r--r--README.md118
-rw-r--r--src/app.c44
-rw-r--r--src/app.h37
-rw-r--r--src/main.c25
-rw-r--r--src/platform.h25
-rw-r--r--src/platform_posix.c171
-rw-r--r--src/shorthand.h35
10 files changed, 568 insertions, 0 deletions
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 @@
1This software is dual licensed under Unlicense/MIT. Choose whichever you prefer.
2
3----------------------------------------------------------------------
4UNLICENSE
5----------------------------------------------------------------------
6
7This is free and unencumbered software released into the public domain.
8
9Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
10software, either in source code form or as a compiled binary, for any purpose,
11commercial or non-commercial, and by any means.
12
13In jurisdictions that recognize copyright laws, the author or authors of this
14software dedicate any and all copyright interest in the software to the public
15domain. We make this dedication for the benefit of the public at large and to
16the detriment of our heirs and successors. We intend this dedication to be an
17overt act of relinquishment in perpetuity of all present and future rights to
18this software under copyright law.
19
20THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
23LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
24CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
27For more information, please refer to <http://unlicense.org/>
28
29
30----------------------------------------------------------------------
31MIT License
32----------------------------------------------------------------------
33
34Copyright (c) 2020 Bad Diode
35
36Permission is hereby granted, free of charge, to any person obtaining a copy of
37this software and associated documentation files (the "Software"), to deal in
38the Software without restriction, including without limitation the rights to
39use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
40the Software, and to permit persons to whom the Software is furnished to do so,
41subject to the following conditions:
42
43The above copyright notice and this permission notice shall be included in all
44copies or substantial portions of the Software.
45
46THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
48FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
49COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
50IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
51CONNECTION 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 @@
1.POSIX:
2.SUFFIXES:
3
4# Source code location and files to watch for changes.
5SRC_DIR := src
6SRC_MAIN := $(SRC_DIR)/main.c
7SRC_APP := $(SRC_DIR)/app.c
8WATCH_SRC := $(wildcard $(SRC_DIR)/*.c)
9WATCH_SRC += $(wildcard $(SRC_DIR)/*.h)
10
11# Output library names and executables.
12BIN_NAME := app
13LIB_NAME := libmic.so
14BUILD_DIR := build
15BIN := $(BUILD_DIR)/$(BIN_NAME)
16LIB := $(BUILD_DIR)/$(LIB_NAME)
17LIB_DIR := $(BUILD_DIR)
18
19# Compiler and linker configuration.
20CC := gcc
21CFLAGS := -Wall -Wextra -pedantic -std=c99
22LDFLAGS :=
23LDLIBS :=
24RELEASE_CFLAGS := -DNDEBUG -O2
25DEBUG_CFLAGS := -DDEBUG -g
26
27.PHONY: dynamic static clean run
28
29# Setup debug/release builds.
30# make clean && make <target> DEBUG=0
31# make clean && make <target> DEBUG=1
32DEBUG ?= 0
33ifeq ($(DEBUG), 1)
34 CFLAGS += $(DEBUG_CFLAGS)
35else
36 CFLAGS += $(RELEASE_CFLAGS)
37endif
38
39dynamic: CFLAGS += -fPIC
40dynamic: CFLAGS += -DLIB_NAME=\"$(LIB_NAME)\"
41dynamic: CFLAGS += -DLIB_DIR=\"$(LIB_DIR)\"
42dynamic: LDFLAGS += -ldl
43dynamic: $(BUILD_DIR) $(LIB) $(BIN)
44
45static: SRC_MAIN += $(SRC_APP)
46static: $(BUILD_DIR) $(BIN)
47
48$(BIN): $(SRC_MAIN) $(WATCH_SRC)
49 $(CC) $(CFLAGS) $(LDFLAGS) -o $(BIN) $(SRC_MAIN) $(LDLIBS)
50
51$(LIB): $(SRC_APP) $(WATCH_SRC)
52 $(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ $(SRC_APP) $(LDLIBS)
53
54$(BUILD_DIR):
55 mkdir -p $(BUILD_DIR)
56
57run: $(BIN)
58 exec $(BIN)
59
60clean:
61 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 @@
1# Micro Interactive C Framework (MIC)
2
3MIC is a tiny (<300 cloc) framework to boostrap interactive C projects.
4Application code can be dynamically reloaded upon compilation, allowing C to be
5used as a pseudo-scripting language. This property can be very useful when
6developing, but might not be always desirable for a final release, for this
7reason, static compilation can be selected as a make target when needed. The
8main limitation of hot code reload is that global state can't be stored in the
9main app code, but the framework is designed so the state is passed around when
10needed and is always accessible for the relevant functions.
11
12This framework can be easily extended to work with multiple platforms. However,
13hot code reload will only be available for platforms that support dynamic
14linking. For the moment, only POSIX compliant platforms are supported but
15Windows is a target for the near future.
16
17Remember that MIC is only a base, you can (and should) modify anything in here
18to adapt it to your particular project.
19
20## Example usage
21
22Compile and execute the program:
23
24```
25make && ./build/app
26```
27
28While the application is running, make changes to the `src/app.c`. For example,
29change the `app_step` function from:
30
31```
32static inline bool
33app_step(AppState *state, PlatformAPI platform) {
34 (void)state; // Unused parameter.
35 platform.log("STEP");
36 platform.sleep(100000);
37 return true;
38}
39```
40
41To:
42
43```
44static inline bool
45app_step(AppState *state, PlatformAPI platform) {
46 (void)state; // Unused parameter.
47 platform.log("Hello world!");
48 platform.sleep(100000);
49 return true;
50}
51```
52
53In another terminal (or using your editor of choice), recompile the program:
54
55```
56make
57```
58
59The changes should take effect in the running application.
60
61## Implementation details
62
63This framework offers two APIs for managing system resources and executing
64application logic. The `PlatformAPI` is described in `src/platform.h` and it's
65tasked with manage the resources of the platform and/or operating system. For
66example, opening/reading files, allocating memory, logging, etc. To support
67a new platform it is sufficient to implement the functions described on the API
68and make sure to select such platform at compile time. Additionally, the
69following internal functions must be implemented for static and dynamic linking:
70
71```
72// Load the dynamic library and initialize application. Returns the success or
73// failure of this operation.
74static bool _app_init(AppAPI *api, AppState *state, PlatformAPI platform);
75
76// This function reloads the application code from the dynamic library,
77// returning `true` if the AppAPI is ready to be used.
78static bool _app_reload();
79
80// Cleanup resources before exit.
81static void _app_destroy(AppAPI *api, AppState *state, PlatformAPI platform);
82```
83
84The `AppAPI`, described in `src/app.h` and handles app initialization and
85destruction, it contains the behaviour in the event of hot code reloading as
86well as the main step function. The `init` function typically will be tasked
87with initial allocation of resources. The duty of `reload` and `unload` is
88application dependent, for example, we might wish to re-compile the shader
89programs or reload geometry/sprites when we make modifications to the main
90application code. The `step` function is called in every iteration of the loop,
91and is where the bulk of the logic will likely be.
92
93The application state is stored in a unique `AppState` structure. MIC has been
94architected so that the state and platform functions are available in the
95previously mentioned `AppAPI` functions. `AppState` can contain whatever state
96is needed and will be persistent when re-compiling. The only limitation is that
97if the `AppState` structure changes, the application must be restarted, as the
98stored state will become invalid. This can be circumvented by pre-allocating
99some memory that can then be partitioned with a pool allocator to suit the
100application needs. This design is also conducive for saving state data by simply
101serializing the `AppState` structure.
102
103## Credits
104
105I initially discovered the hot code reloading trick when watching [Casey
106Muratori's][casey] [Handmade Hero][handmade] series. It blew my mind! However,
107the code he presented was focused solely on Windows and I never end up trying it
108myself. Some years later I tried to find a way of making this work in Mac/Linux
109when I came across [Chris Wellons'][skeeto] take on this problem with a clever
110function pointer API in his [interactive c demo][interac-c-demo] page. This
111framework uses a lot of the ideas he presents there, as you can clearly see if
112you look at [his code][interac-c-demo].
113
114[casey]: https://caseymuratori.com
115[handmade]: https://handmadehero.org
116[skeeto]: https://nullprogram.com
117[interac-c-demo]: https://nullprogram.com/blog/2014/12/23/
118[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 @@
1#include "app.h"
2#include "platform.h"
3
4static inline bool
5app_init(AppState *state, PlatformAPI platform) {
6 platform.log("INIT");
7 state->lt_memory = platform.calloc(LT_MEMORY_SIZE, sizeof(u8));
8 state->st_memory = platform.calloc(ST_MEMORY_SIZE, sizeof(u8));
9 return true;
10}
11
12static inline void
13app_destroy(AppState *state, PlatformAPI platform) {
14 (void)state; // Unused parameter.
15 platform.log("DESTROY");
16}
17
18static inline void
19app_reload(AppState *state, PlatformAPI platform) {
20 (void)state; // Unused parameter.
21 platform.log("RELOAD");
22}
23
24static inline void
25app_unload(AppState *state, PlatformAPI platform) {
26 (void)state; // Unused parameter.
27 platform.log("UNLOAD");
28}
29
30static inline bool
31app_step(AppState *state, PlatformAPI platform) {
32 (void)state; // Unused parameter.
33 platform.log("STEP");
34 platform.sleep(100000);
35 return true;
36}
37
38const AppAPI APP_API = {
39 .init = app_init,
40 .destroy = app_destroy,
41 .reload = app_reload,
42 .step = app_step,
43 .unload = app_unload,
44};
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 @@
1#ifndef MIC_APP_H
2#define MIC_APP_H
3
4#include "shorthand.h"
5#include "platform.h"
6
7#define LT_MEMORY_SIZE GB(2)
8#define ST_MEMORY_SIZE MB(100)
9
10typedef struct AppState {
11 // Long and short term memory.
12 char *lt_memory;
13 char *st_memory;
14} AppState;
15
16// Function pointers for the AppAPI.
17typedef struct AppAPI {
18 // Initialization code. Meant to be called exactly once before other API
19 // interactions. Returns the success or failure of the initialization
20 // process.
21 bool (*init)(AppState *state, PlatformAPI platform);
22
23 // Resource deallocation and cleanup. Meant to be called exactly once at the
24 // end of the app.
25 void (*destroy)(AppState *state, PlatformAPI platform);
26
27 // In case of hot code reloading, these functions handle the necessary
28 // resource setup and reloading.
29 void (*reload)(AppState *state, PlatformAPI platform);
30 void (*unload)(AppState *state, PlatformAPI platform);
31
32 // Main update/step function for the app. Returns if the app should keep
33 // running.
34 bool (*step)(AppState *state, PlatformAPI platform);
35} AppAPI;
36
37#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 @@
1#include "platform_posix.c"
2
3int
4main(void) {
5 // App initialization.
6 AppAPI api = {0};
7 AppState state = {0};
8 if (!_app_init(&api, &state, PLATFORM_API)) {
9 return EXIT_FAILURE;
10 }
11
12 // Main loop.
13 for (;;) {
14 if (!_app_reload(&api, &state, PLATFORM_API)) {
15 continue;
16 }
17 if (!api.step(&state, PLATFORM_API)) {
18 break;
19 }
20 }
21
22 // Cleanup.
23 _app_destroy(&api, &state, PLATFORM_API);
24 return EXIT_SUCCESS;
25}
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 @@
1#ifndef MIC_PLATFORM_H
2#define MIC_PLATFORM_H
3
4// Function pointers for the PlatformAPI. This allows the app to call platform
5// specific functions that perform IO, memory allocations, etc.
6typedef struct PlatformAPI {
7 // Reads an entire file into a null terminated buffer. It doesn't perform
8 // memory allocations and may crash if there is not enough memory or if it
9 // is uninitialized. Returns the number of bytes read.
10 size_t (*read_file)(const char *path, char *memory);
11
12 // Custom memory allocation functions for the platform.
13 void *(*malloc)(size_t size);
14 void (*free)(void *ptr);
15 void *(*calloc)(size_t nmemb, size_t size);
16 void *(*realloc)(void *ptr, size_t size);
17
18 // Sleep/wait for a given number of microseconds.
19 void (*sleep)(size_t microseconds);
20
21 // Logging functions.
22 void (*log)(const char *format, ...);
23} PlatformAPI;
24
25#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 @@
1#define _DEFAULT_SOURCE
2#include <stdarg.h>
3#include <stdio.h>
4#include <stdlib.h>
5#include <time.h>
6#include <unistd.h>
7
8#include "app.h"
9
10//
11// PlatformAPI Implementation.
12//
13
14size_t
15platform_read_file(const char *path, char *memory) {
16 size_t file_size = 0;
17 FILE *fp = fopen(path, "rb");
18 if (!fp) {
19 return 0;
20 }
21 fseek(fp, 0, SEEK_END);
22 file_size = ftell(fp);
23 rewind(fp);
24 fread(memory, 1, file_size, fp);
25 fclose(fp);
26 memory[file_size] = '\0';
27 return file_size;
28}
29
30void
31platform_log(const char *format, ...) {
32 // Print date.
33 time_t raw_time = time(NULL);
34 struct tm *tm = localtime(&raw_time);
35 char date[64];
36 strftime(date, sizeof(date), "%Y-%M-%d | %H:%M:%S", tm);
37 printf("%s | ", date);
38
39 // Print message.
40 va_list args;
41 va_start(args, format);
42 vprintf(format, args);
43
44 printf("\n");
45 va_end(args);
46}
47
48void
49platform_sleep(size_t microseconds) {
50 usleep(microseconds);
51}
52
53#if defined(LIB_NAME) && defined(LIB_DIR)
54
55//
56// Dynamic linking with hot code reload.
57//
58
59#include <dlfcn.h>
60#include <sys/stat.h>
61#include <sys/types.h>
62
63static ino_t lib_id;
64static void * lib_handle;
65
66static const char *lib_path = LIB_DIR "/" LIB_NAME;
67
68static bool
69load_lib(AppAPI *api) {
70 struct stat st;
71 if (stat(lib_path, &st) != 0) {
72 return false;
73 }
74
75 if (lib_id != st.st_ino) {
76 void *handle = dlopen(lib_path, RTLD_NOW);
77 if (!handle) {
78 lib_handle = NULL;
79 lib_id = 0;
80 return false;
81 }
82 lib_handle = handle;
83 lib_id = st.st_ino;
84
85 const AppAPI *app_api = dlsym(lib_handle, "APP_API");
86 if (app_api == NULL) {
87 dlclose(lib_handle);
88 lib_handle = NULL;
89 lib_id = 0;
90 return false;
91 }
92 *api = *app_api;
93 }
94
95 return true;
96}
97
98static bool
99_app_reload(AppAPI *api, AppState *state, PlatformAPI platform) {
100 struct stat st;
101 if (stat(lib_path, &st) == 0 && lib_id != st.st_ino) {
102 if (lib_handle) {
103 api->unload(state, platform);
104 dlclose(lib_handle);
105 }
106
107 if (!load_lib(api)) {
108 return false;
109 }
110 api->reload(state, platform);
111 }
112
113 return true;
114}
115
116static bool
117_app_init(AppAPI *api, AppState *state, PlatformAPI platform) {
118 if (!load_lib(api)) {
119 fprintf(stderr, "error: can't open app library file: %s\n", lib_path);
120 return false;
121 }
122 api->init(state, platform);
123 return true;
124}
125
126static void
127_app_destroy(AppAPI *api, AppState *state, PlatformAPI platform) {
128 api->destroy(state, platform);
129 if (lib_handle) {
130 dlclose(lib_handle);
131 lib_handle = NULL;
132 lib_id = 0;
133 }
134}
135
136#else
137
138//
139// Static linking of app code.
140//
141
142extern const AppAPI APP_API;
143
144static bool
145_app_reload() {
146 return true;
147}
148
149static bool
150_app_init(AppAPI *api, AppState *state, PlatformAPI platform) {
151 *api = APP_API;
152 api->init(state, platform);
153 return true;
154}
155
156static void
157_app_destroy(AppAPI *api, AppState *state, PlatformAPI platform) {
158 api->destroy(state, platform);
159}
160
161#endif
162
163const PlatformAPI PLATFORM_API = {
164 .read_file = platform_read_file,
165 .malloc = malloc,
166 .free = free,
167 .calloc = calloc,
168 .realloc = realloc,
169 .log = platform_log,
170 .sleep = platform_sleep,
171};
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 @@
1#ifndef MIC_SHORTHAND_H
2#define MIC_SHORTHAND_H
3
4#include <assert.h>
5#include <stdbool.h>
6#include <stddef.h>
7#include <stdint.h>
8
9//
10// This simple header just typedefs the basic C define types to a shorter name,
11// loads the quality of life bool macro for _Bool and defines shorthand macros
12// for byte sizes. We need that the targeted architecture uses the floating
13// point representation as described on the IEEE-754 standard.
14//
15
16_Static_assert(sizeof(double) == 8, "no support for IEEE-754");
17_Static_assert(sizeof(float) == 4, "no support for IEEE-754");
18
19typedef uint8_t u8;
20typedef uint16_t u16;
21typedef uint32_t u32;
22typedef uint64_t u64;
23typedef int8_t s8;
24typedef int16_t s16;
25typedef int32_t s32;
26typedef int64_t s64;
27typedef float f32;
28typedef double f64;
29
30#define KB(N) ((u64)(N) * 1024)
31#define MB(N) ((u64)KB(N) * 1024)
32#define GB(N) ((u64)MB(N) * 1024)
33#define TB(N) ((u64)GB(N) * 1024)
34
35#endif // MIC_SHORTHAND_H