summaryrefslogtreecommitdiff
path: root/chapter31/src
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-11-11 11:49:04 -0800
committerCliff Click <cliffc@acm.org>2026-11-12 13:48:30 -0800
commitf4bac750504ae2d21068a87b6a6bf4a733476993 (patch)
tree644741a31d045d17f9456f07078a2fca2bb21412 /chapter31/src
parenta269fa7e8aea51b4fbdd2f2c6c24e84ef26f9ca4 (diff)
downloadSimple-f4bac750504ae2d21068a87b6a6bf4a733476993.tar.gz
Simple-f4bac750504ae2d21068a87b6a6bf4a733476993.zip
First cut graphics
Diffstat (limited to 'chapter31/src')
-rw-r--r--chapter31/src/main/c/simple_graphics.c60
-rw-r--r--chapter31/src/main/c/simple_graphics.h19
-rw-r--r--chapter31/src/main/c/simple_minifb.c113
-rw-r--r--chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java12
-rw-r--r--chapter31/src/main/smp/sys.smp1
-rw-r--r--chapter31/src/main/smp/sys/gfx.smp38
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java4
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java69
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java16
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java2
-rw-r--r--chapter31/src/test/java/com/seaofnodes/simple/TestC.java3
11 files changed, 333 insertions, 4 deletions
diff --git a/chapter31/src/main/c/simple_graphics.c b/chapter31/src/main/c/simple_graphics.c
new file mode 100644
index 00000000..41073470
--- /dev/null
+++ b/chapter31/src/main/c/simple_graphics.c
@@ -0,0 +1,60 @@
+#include "simple_graphics.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dlfcn.h>
+
+// No MiniFB headers or link dependency in the default runtime. Cygwin's
+// dlopen also loads native Windows DLLs; both use the x64 Windows C ABI.
+static const struct simple_graphics_api *api;
+static void *library;
+
+static int load_graphics(void) {
+ // Explicitly disabling graphics also makes headless tests deterministic.
+ const char *path = getenv("SIMPLE_MINIFB");
+ if (path && strcmp(path, "off") == 0) {
+ fprintf(stderr, "Simple graphics: disabled (SIMPLE_MINIFB=off).\n");
+ return -1;
+ }
+ if (api) return 0;
+ if (!path || !*path) {
+ fprintf(stderr, "Simple graphics: MiniFB backend not configured. "
+ "Run make graphics, then set SIMPLE_MINIFB to the backend library's full path.\n");
+ return -1;
+ }
+ library = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+ if (!library) {
+ fprintf(stderr, "Simple graphics: cannot load MiniFB backend '%s': %s\n", path, dlerror());
+ return -1;
+ }
+ const struct simple_graphics_api *(*get_api)(void) =
+ (const struct simple_graphics_api *(*)(void))dlsym(library, "simple_minifb_api");
+ const struct simple_graphics_api *candidate = get_api ? get_api() : NULL;
+ if (!candidate || candidate->version != SIMPLE_GRAPHICS_VERSION ||
+ !candidate->open || !candidate->present || !candidate->poll ||
+ !candidate->mouse_x || !candidate->mouse_y || !candidate->button ||
+ !candidate->key || !candidate->close) {
+ fprintf(stderr, "Simple graphics: incompatible MiniFB backend '%s'. Rebuild with make graphics.\n", path);
+ dlclose(library);
+ library = NULL;
+ return -1;
+ }
+ api = candidate;
+ // Keep the library loaded until process exit; its window callbacks may
+ // still be registered. Closing a window releases its native resources.
+ atexit(api->close);
+ return 0;
+}
+
+int32_t simple_gfx_open(const char *title, uint32_t width, uint32_t height) {
+ return load_graphics() ? -1 : api->open(title, width, height);
+}
+int32_t simple_gfx_present(const uint32_t *pixels, uint32_t count) {
+ return api ? api->present(pixels, count) : -1;
+}
+int32_t simple_gfx_poll(int32_t wait) { return api ? api->poll(wait) : -1; }
+int32_t simple_gfx_mouse_x(void) { return api ? api->mouse_x() : 0; }
+int32_t simple_gfx_mouse_y(void) { return api ? api->mouse_y() : 0; }
+int32_t simple_gfx_button(int32_t button) { return api ? api->button(button) : 0; }
+int32_t simple_gfx_key(int32_t key) { return api ? api->key(key) : 0; }
+int32_t simple_gfx_close(void) { if (api) api->close(); return 0; }
diff --git a/chapter31/src/main/c/simple_graphics.h b/chapter31/src/main/c/simple_graphics.h
new file mode 100644
index 00000000..35a0aa08
--- /dev/null
+++ b/chapter31/src/main/c/simple_graphics.h
@@ -0,0 +1,19 @@
+#ifndef SIMPLE_GRAPHICS_H
+#define SIMPLE_GRAPHICS_H
+#include <stdint.h>
+
+// Private, versioned boundary between the tiny runtime loader and MiniFB.
+// All calls run on the main thread. Pixel memory is borrowed for present only.
+struct simple_graphics_api {
+ int32_t version;
+ int32_t (*open)(const char *, uint32_t, uint32_t);
+ int32_t (*present)(const uint32_t *, uint32_t);
+ int32_t (*poll)(int32_t);
+ int32_t (*mouse_x)(void);
+ int32_t (*mouse_y)(void);
+ int32_t (*button)(int32_t);
+ int32_t (*key)(int32_t);
+ void (*close)(void);
+};
+#define SIMPLE_GRAPHICS_VERSION 1
+#endif
diff --git a/chapter31/src/main/c/simple_minifb.c b/chapter31/src/main/c/simple_minifb.c
new file mode 100644
index 00000000..34663e2e
--- /dev/null
+++ b/chapter31/src/main/c/simple_minifb.c
@@ -0,0 +1,113 @@
+#include "simple_graphics.h"
+#include <MiniFB.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <errno.h>
+#include <time.h>
+#endif
+
+// One fixed-size window for now. No Simple pointer survives a native call:
+// MiniFB retains its last draw buffer for OS repaint events, so copy it here.
+static struct mfb_window *window;
+static uint32_t *frame;
+static uint32_t width, height;
+
+// The C-only build never installs the upstream C++ callback wrappers.
+void release_cpp_stub(struct mfb_window *unused) { (void)unused; }
+
+static void close_window(void) {
+ if (window) {
+ mfb_close(window); // Requests close.
+ mfb_update_events(window); // Actually destroys the window.
+ window = NULL;
+ }
+ free(frame);
+ frame = NULL;
+}
+
+static int32_t open_window(const char *title, uint32_t w, uint32_t h) {
+ if (window || !w || !h || w > UINT32_MAX / 4 ||
+ (uint64_t)w * h > UINT32_MAX || (uint64_t)w * h > SIZE_MAX / 4) {
+ fprintf(stderr, "Simple graphics: invalid dimensions or a window is already open.\n");
+ return -1;
+ }
+ frame = calloc((size_t)w * h, sizeof(*frame));
+ if (!frame) {
+ fprintf(stderr, "Simple graphics: cannot allocate the display buffer.\n");
+ return -1;
+ }
+ window = mfb_open(title, w, h);
+ if (!window) {
+ fprintf(stderr, "Simple graphics: MiniFB could not open a window (is a graphical display available?).\n");
+ close_window();
+ return -1;
+ }
+ width = w;
+ height = h;
+ return 0;
+}
+
+static int32_t status(mfb_update_state state) {
+ if (state == MFB_STATE_OK) return 0;
+ if (state == MFB_STATE_EXIT) {
+ window = NULL; // MiniFB has already destroyed it.
+ close_window();
+ return 1;
+ }
+ fprintf(stderr, "Simple graphics: MiniFB update failed (%d).\n", state);
+ close_window();
+ return -1;
+}
+
+static int32_t present(const uint32_t *pixels, uint32_t count) {
+ if (!window) return 1;
+ if (!pixels || count != (uint64_t)width * height) {
+ fprintf(stderr, "Simple graphics: pixel array length must equal width * height.\n");
+ return -1;
+ }
+ memcpy(frame, pixels, (size_t)count * sizeof(*frame));
+ return status(mfb_update_ex(window, frame, width, height));
+}
+
+static int32_t poll_events(int32_t wait) {
+ if (!window) return 1;
+ if (wait) {
+ // mfb_wait_sync returns false for both destruction and errors that
+ // leave the window alive. Use a short OS wait, then the status-bearing
+ // event API, so we know whether the window still needs destruction.
+#ifdef _WIN32
+ Sleep(17);
+#else
+ struct timespec delay = {0, 16666667};
+ while (nanosleep(&delay, &delay) < 0) {
+ if (errno != EINTR) {
+ perror("Simple graphics: event wait");
+ close_window();
+ return -1;
+ }
+ }
+#endif
+ }
+ return status(mfb_update_events(window));
+}
+static int32_t mouse_x(void) { return window ? mfb_get_mouse_x(window) : 0; }
+static int32_t mouse_y(void) { return window ? mfb_get_mouse_y(window) : 0; }
+static int32_t button(int32_t b) {
+ return window && b >= 0 && b < 8 ? mfb_get_mouse_button_buffer(window)[b] != 0 : 0;
+}
+static int32_t key(int32_t k) {
+ return window && k >= 0 && k <= MFB_KB_KEY_LAST ? mfb_get_key_buffer(window)[k] != 0 : 0;
+}
+
+static const struct simple_graphics_api api = {
+ SIMPLE_GRAPHICS_VERSION, open_window, present, poll_events,
+ mouse_x, mouse_y, button, key, close_window
+};
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
diff --git a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
index ae4ecb74..79bebe1f 100644
--- a/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
+++ b/chapter31/src/main/java/com/seaofnodes/simple/codegen/Serialize.java
@@ -417,7 +417,7 @@ public class Serialize {
assert elf._clz._name==strs[1/*skip null ptr*/];
for( Type type : types )
if( type instanceof TypeStruct ts )
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
for( Type type : types ) {
if( !(type instanceof TypeStruct ts) || !TypeStruct.startsClzPrefix(ts._name) )
continue;
@@ -429,7 +429,7 @@ public class Serialize {
elf._pubTypes = new TypeStruct[npubTypes];
for( int i=0; i<npubTypes; i++ ) {
TypeStruct ts = elf._pubTypes[i] = (TypeStruct)types[pubTypeIds[i]];
- Parser.TYPES.put(ts._name,ts);
+ installType(ts);
}
elf._functions = new FunctionSummary[bais.packed4()];
for( int i = 0; i<elf._functions.length; i++ ) {
@@ -444,6 +444,14 @@ public class Serialize {
return elf._types = new TypeRead(types,strs,fileAliases,fileFidxs,fileRpcs);
}
+ private static void installType(TypeStruct ts) {
+ // TYPES names the mutable array declaration. Read-only arrays in
+ // imported signatures keep their own types, but must not replace that
+ // declaration: closeOver looks up its mutable variant by name.
+ if( ts.isAry() ) ts = ts.replace(ts.field("[]").makeFrom(false));
+ Parser.TYPES.put(ts._name,ts);
+ }
+
static void readAll( CodeGen code, ElfReader elf, GlobalBits aliases, GlobalBits fidxs, GlobalBits rpcs ) {
BAOS bais = elf._bais;
TypeRead tread = readTypes(code, elf, aliases, fidxs, rpcs);
diff --git a/chapter31/src/main/smp/sys.smp b/chapter31/src/main/smp/sys.smp
index 1201315b..deaafa0b 100644
--- a/chapter31/src/main/smp/sys.smp
+++ b/chapter31/src/main/smp/sys.smp
@@ -7,6 +7,7 @@ val aryi64 = aryi64; // Variable sized array of i64
val aryu8 = aryu8; // Variable sized array of u8
val char = char; // Character utilities
val io = io; // IO library
+val gfx = gfx; // Optional pixel window (MiniFB)
val text = text; // Typed text conversions
val libc = libc; // libc FFI bindings
val rt = rt; // Run Time support
diff --git a/chapter31/src/main/smp/sys/gfx.smp b/chapter31/src/main/smp/sys/gfx.smp
new file mode 100644
index 00000000..1f0e3ca9
--- /dev/null
+++ b/chapter31/src/main/smp/sys/gfx.smp
@@ -0,0 +1,38 @@
+// Minimal pixel-window interface; drawing belongs in Simple.
+// One fixed-size window, main thread only. Pixel format is 0x00RRGGBB.
+// open/present/poll return OK, CLOSED (normal user close/Esc), or ERROR.
+int OK = 0;
+int CLOSED = 1;
+int ERROR = -1;
+int LEFT = 1;
+int ESCAPE = 256;
+
+{ i64 u32 u32 -> i32 } simple_gfx_open = "C";
+{ i64 u32 -> i32 } simple_gfx_present = "C";
+{ i32 -> i32 } simple_gfx_poll = "C";
+{ i32 } simple_gfx_mouse_x = "C";
+{ i32 } simple_gfx_mouse_y = "C";
+{ i32 -> i32 } simple_gfx_button = "C";
+{ i32 -> i32 } simple_gfx_key = "C";
+{ i32 } simple_gfx_close = "C";
+
+// Failure prints a diagnostic to stderr and returns ERROR; callers decide
+// whether to exit or fall back to text. No native library is loaded until here.
+val open = { u8[~] title, u32 width, u32 height ->
+ i64 ptr = aryu8.str0(title);
+ simple_gfx_open(ptr,width,height);
+};
+// Array length must be width*height. The native layer copies for OS repaints;
+// it never keeps a pointer into the Simple heap after this call returns.
+val present = { u32[~] pixels ->
+ i64 ptr = pixels;
+ simple_gfx_present(ptr,pixels#);
+};
+// wait=true waits about 17ms before polling, avoiding a busy loop.
+val poll = { bool wait -> simple_gfx_poll(wait); };
+val mouseX = { -> simple_gfx_mouse_x(); };
+val mouseY = { -> simple_gfx_mouse_y(); };
+val button = { int button -> simple_gfx_button(button); };
+// Printable keys use uppercase ASCII; special keys use MiniFB's key codes.
+val key = { int key -> simple_gfx_key(key); };
+val close = { -> simple_gfx_close(); };
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
index 67e7f982..44cdbada 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/Chapter25Test.java
@@ -475,9 +475,9 @@ public class Chapter25Test {
// Elf files are sane
- // Sys depends on io, libc, text, char, collections, and array utilities.
+ // Sys includes the graphics interface; its native backend is optional.
assertArrayEquals(new String[]{"sys/adt", "sys/ary",
- "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
+ "sys/aryi64", "sys/aryu8", "sys/char", "sys/gfx", "sys/io", "sys/libc", "sys/rt", "sys/scan", "sys/text"},sys_elf._deps);
assertArrayEquals(new String[]{"sys"},sys_elf._units);
assertSame("class:sys" ,sys_elf._clz._name);
}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
new file mode 100644
index 00000000..2e9c8dbc
--- /dev/null
+++ b/chapter31/src/test/java/com/seaofnodes/simple/GraphicsTest.java
@@ -0,0 +1,69 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.util.Ary;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+// These tests never open a window and do not need MiniFB installed.
+public class GraphicsTest {
+ private static String graphics, text;
+
+ @BeforeClass public static void compilePrograms() throws Exception {
+ var paths = new Ary<>(new String[]{TestC.RELEASE_SYS_DIR});
+ graphics = TestC.compile(Files.readString(Path.of("docs/examples/Graphics.smp")),
+ "graphics_missing",paths,TestC.CALL_CONVENTION,null,null,-1);
+ text = TestC.compile("val main={ -> sys.io.p(\"text still works\\n\"); return 0; };",
+ "graphics_text",paths,TestC.CALL_CONVENTION,null,null,-1);
+ }
+
+ private static String run(String exe, String backend, int exit) throws Exception {
+ ProcessBuilder pb = new ProcessBuilder(exe).redirectErrorStream(true);
+ if( backend == null ) pb.environment().remove("SIMPLE_MINIFB");
+ else pb.environment().put("SIMPLE_MINIFB",backend);
+ Process child = pb.start();
+ try {
+ assertTrue("native program timed out",child.waitFor(10,TimeUnit.SECONDS));
+ String output = new String(child.getInputStream().readAllBytes(),java.nio.charset.StandardCharsets.UTF_8);
+ assertEquals(output,exit,child.exitValue());
+ return output;
+ } finally { if( child.isAlive() ) child.destroyForcibly(); }
+ }
+
+ @Test public void missingConfiguration() throws Exception {
+ String output = run(graphics,null,1);
+ assertTrue(output,output.contains("MiniFB backend not configured"));
+ assertTrue(output,output.contains("make graphics"));
+ }
+
+ @Test public void disabledGraphics() throws Exception {
+ assertTrue(run(graphics,"off",1).contains("disabled"));
+ }
+
+ @Test public void missingLibrary() throws Exception {
+ String missing = Path.of("build","no-such-minifb-backend").toAbsolutePath().toString();
+ String output = run(graphics,missing,1);
+ assertTrue(output,output.contains("cannot load MiniFB backend"));
+ }
+
+ @Test public void textIgnoresGraphicsConfiguration() throws Exception {
+ assertEquals("text still works\n",run(text,"off",0));
+ assertEquals("text still works\n",run(text,"no-such-backend",0));
+ }
+
+ @Test public void incompatibleLibrary() throws Exception {
+ Path source = Path.of("build/objs/graphics_incompatible.c");
+ Path library = Path.of("build/objs/graphics_incompatible" +
+ (TestC.OS.startsWith("Windows") ? ".dll" : ".so"));
+ Files.writeString(source,"""
+ #include "simple_graphics.h"
+ static const struct simple_graphics_api api = {999};
+ const struct simple_graphics_api *simple_minifb_api(void) { return &api; }
+ """);
+ TestC.exec(30,"gcc","-shared","-fPIC","-Isrc/main/c",source.toString(),"-o",library.toString());
+ assertTrue(run(graphics,library.toAbsolutePath().toString(),1).contains("incompatible MiniFB backend"));
+ }
+}
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
index 1853aa48..1c278791 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java
@@ -10,6 +10,22 @@ import org.junit.Test;
import static org.junit.Assert.*;
public class SeparateCompilationTest {
+ @Test public void testImportedReadOnlyPrimitiveArray() throws Exception {
+ Path build=Path.of("build/objs/separate");
+ Files.createDirectories(build);
+ Path lib=Files.createTempDirectory(build,"pixels-");
+ new CodeGen(null,lib.toString(),null,"Pixels",
+ "val first={ u32[~] pixels -> pixels[0]; };",123,true,TypeInteger.BOT)
+ .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false);
+ var paths=new Ary<>(new String[]{lib.toString()});
+ CodeGen code=new CodeGen(null,"build/objs",paths,"PixelClient","""
+ val pixels=new u32[2];
+ pixels[0]=4294967295;
+ return Pixels.first(pixels);
+ """,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4294967295",Eval2.eval(code,0));
+ }
+
@Test public void testImportedNativeExposure() throws Exception {
Path build=Path.of("build/objs/separate");
Files.createDirectories(build);
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
index f3491314..2aa2e3da 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/StreamIOTest.java
@@ -131,6 +131,8 @@ public class StreamIOTest extends TestC {
gcc.addAll(linkObjs(code));
gcc.add(runtimeObject("simple_crt"));
gcc.add(runtimeObject("simple_args"));
+ gcc.add(runtimeObject("simple_graphics"));
+ if( !OS.startsWith("Windows") ) gcc.add("-ldl");
gcc.addAll(new String[]{"-lm","-o",exe});
exec(TEST_TIMEOUT_SECONDS,gcc.asAry());
assertEquals("ok\n",exec(TEST_TIMEOUT_SECONDS,exe));
diff --git a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
index dd7999c9..73795111 100644
--- a/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
+++ b/chapter31/src/test/java/com/seaofnodes/simple/TestC.java
@@ -190,6 +190,9 @@ public abstract class TestC {
params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie");
if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main`
params.addAll(objects);
+ // The loader has no MiniFB dependency and opens nothing until gfx.open.
+ params.add(runtimeObject("simple_graphics"));
+ if( !OS.startsWith("Windows") ) params.add("-ldl");
// Standalone Simple programs export `simple_main`; the small C
// runtime owns native main and calls the Simple argument converter
// before making the ABI-qualified entry call.