This is a minimal example of using Bun’s FFI module to render a GUI using GLFW and OpenGL2.

Bun FFI GUI example

I’ve been keeping an eye on BunJS for a while now because if its ability to compile to single file executables. Given Bun’s recent acquisition, I though it’d be interesting to explore the possibilities of writing GUIs using typescript without the DOM. This is a very basic example of using C bindings to do that.

The meat of it is here. Just defining the C externs.

import { dlopen, FFIType, ptr, suffix } from "bun:ffi";

const glfwPath =
  process.platform === "darwin"
    ? "/opt/homebrew/lib/libglfw.dylib"
    : `libglfw.${suffix}`;
const glfw = dlopen(glfwPath, {
  glfwInit: { args: [], returns: FFIType.i32 },
  glfwTerminate: { args: [], returns: FFIType.void },
  glfwCreateWindow: {
    args: [FFIType.i32, FFIType.i32, FFIType.cstring, FFIType.ptr, FFIType.ptr],
    returns: FFIType.ptr,
  },
  glfwMakeContextCurrent: { args: [FFIType.ptr], returns: FFIType.void },
  glfwWindowShouldClose: { args: [FFIType.ptr], returns: FFIType.i32 },
  glfwSwapBuffers: { args: [FFIType.ptr], returns: FFIType.void },
  glfwPollEvents: { args: [], returns: FFIType.void },
});

Then you just sorta… call them.

const glPath =
  process.platform === "darwin"
    ? "/System/Library/Frameworks/OpenGL.framework/OpenGL"
    : `libGL.${suffix}`;

const gl = dlopen(glPath, {
  glClearColor: {
    args: [FFIType.f32, FFIType.f32, FFIType.f32, FFIType.f32],
    returns: FFIType.void,
  },
  glClear: { args: [FFIType.u32], returns: FFIType.void },
});

if (!glfw.symbols.glfwInit()) {
  throw new Error("Failed to initialize GLFW");
}

const title = Buffer.from("bun + glfw + opengl");
const window = glfw.symbols.glfwCreateWindow(800, 600, ptr(title), null, null);

if (!window) {
  glfw.symbols.glfwTerminate();
  throw new Error("failed to create window");
}

glfw.symbols.glfwMakeContextCurrent(window);

console.log("window opened...");

const GL_COLOR_BUFFER_BIT = 0x00004000;

while (!glfw.symbols.glfwWindowShouldClose(window)) {
  gl.symbols.glClearColor(0.6, 0.5, 0.9, 1.0);
  gl.symbols.glClear(GL_COLOR_BUFFER_BIT);

  glfw.symbols.glfwSwapBuffers(window);
  // need to do this or we exit right away
  glfw.symbols.glfwPollEvents();
}

// if we exited it's because we got a Ctl+C or closed the window, terminate
glfw.symbols.glfwTerminate();
console.log("exiting");

You can run this with bun run bun-gui.ts, and it works just fine. (Granted, you have to have glfw installed, which you can do with brew.)

This sort of thing interests me. I think there’s a lot to be gained by writing native, cross-platform UIs in JS. We can get away from the DOM if we want to. Major companies, projects, and frameworks have already done it. Could we do it with Bun by writing a comptime interface for OpenGL/WebGL for writing GUIs, and shipping it cross platform via WASM? TBD. Interesting times.