Bun C Plugin

GitHubhttps://github.com/mastermakrela/bun-plugin-c
JSRhttps://jsr.io/@mastermakrela/bun-plugin-c

What if you could just:

import lib, { add } from './lib.c';

const sum = add(1, 2.5); // sum is 3

lib.hello(); // does whatever the C function does

in your TypeScript code?

How and why?

The goal of a bundler is to bundle code structured in a way that makes sense during development to something that makes sense in production. There is lots of compiling, optimising, etc. involved, but this isn’t relevant now.

The part that interests us is the translation of one language to another. Usually it’s something like:

  • TypeScript to JavaScript
  • Svelte to JavaScript
  • React (.tsx) to JavaScript
  • etc.
You see the pattern, right?

But we are used to importing other things, like .css, .svg, .png etc. How does this work if at the end the bundle wants JS? 1

Well, Bun lets you write plugins. In short, they let you register function that is called with a file with an extension is being imported, and it has to somehow give something back that the bundler can understand (usually a string or an object).

index.ts:

import lib from './lib.c';
// more code here...

plugin.ts:

import { plugin, type BunPlugin } from 'bun';

plugin({
	name: 'Custom loader',
	setup(build) {
		build.onLoad({ filter: /.c$/ }, async ({ path }) => {
			// called with `/path/to/lib.c`
		});
	}
});

Okay, so how do we give something useful back? Well, as we’re dealing with C, we should probably first compile it and then use FFI to call the functions.

Luckily in Bun there is an even simpler way, thanks to it’s built in C compiler, we just have to give it a path to a file, and we get our symbols back as a JS object.

//  more code before...
build.onLoad({ filter: /.c$/ }, async (args) => {
	const { symbols: exports } = cc({
		source: args.path,
		symbols
	});

	return exports;
});
/// more code here...

Great! That was easy. So are we done?

Not quite, you’ve probably noticed the symbols argument passed to cc, which is undefined in this code snippet. This object tells the compiler which symbols to export and what their types are (think .h files in C).

Then let us just get those symbols! That’s where the fun begins.

For the proof of concept, I’ve used a rudimentary parser written in JS, to see if this approach is even viable. It currently supports only some function definitions (e.g., it doesn’t like function pointers).

But it works, and you can try it out yourself here.

Additionally, because we had to extract the type information for all the functions anyway, it gives you a TypeScript interface that you can then use in your code.

Improvements over cc

But why not just use Bun’s cc directly in your code?

  1. Ergonomics: What is nicer to write? One import or a whole file wrapping the compile step?

  2. Convenience: If the underlying C file changes, you don’t have to change anything in your TS code. Just call the newly defined function as the symbols are detected automatically.

  3. Type safety: Bun’s cc gives you Record<string, FFIFunction>, which means you don’t get any autocompletion, type checking or anything. This plugin at least gives you an interface that you can use in your code.

Conclusion

This was a fun little experiment, and it showed me that there might be something more interesting here to explore.

If I find time, I’d love to replace the JS based parser with a real deal written in a native language (Native plugins).

It would also be interesting to find a way to make the generated types just work™, without any copying and additional work.

If I come to improving this plugin, I’ll write another article about it here, so if you’re interested in that, bookmark this page and check it in 1-3 months. (Or follow me on GitHub I should probably get some real social media going.)


1 Yes this is a simplification in case of [bun](https://bun.sh/docs/bundler/loaders). But they still use plugins, just built-in ones.


Created on . Last updated on .
  • bun
  • c
  • plugin
  • typescript
  • compiler
  • bundler
Content width
© 2023 - 2025 mastermakrela