1
0
Fork 0

Import C/CPP files

This commit is contained in:
Pabloader 2024-07-09 21:01:37 +00:00
parent adb8b4549e
commit 665892a798
5 changed files with 236 additions and 31 deletions

View File

@ -96,6 +96,16 @@ bun run build
- Example: `src/games/playground/awoo.wasm.ts`
- Triggered by file name `*.wasm.ts`
- Import `*.c`/`*.cpp` files (compile to wasm on the fly)
- Example: `src/games/life/life.c`
- To use, `clang` and wasm toochain should be present in the system
```bash
sudo apt install clang lld wabt
```
- Supports only function exports & `memory`
- exported all non-static functions
- no stdlib
## Publishing

View File

@ -1,4 +1,4 @@
import { plugin, type BunPlugin } from "bun";
import { plugin, $, type BunPlugin } from "bun";
import path from 'path';
import asc from 'assemblyscript/asc';
@ -11,40 +11,58 @@ const wasmPlugin = ({ production, portable }: WasmLoaderConfig = {}): BunPlugin
const p: BunPlugin = {
name: "WASM loader",
async setup(build) {
build.onLoad({ filter: /\.wasm\.ts$/ }, async (args) => {
if (portable) {
const contents = await Bun.file(args.path).text();
return {
contents: `import "assemblyscript/std/portable/index.js";\n${contents}`,
loader: 'tsx',
build.onLoad({ filter: /\.(c(pp)?|wasm(\.ts)?)$/ }, async (args) => {
let wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm');
let jsContent: string = `
async function instantiate(url) {
const { instance } = await WebAssembly.instantiateStreaming(fetch(url));
return {
...instance.exports,
data: new DataView(instance.exports.memory.buffer),
};
}
const module = await instantiate(new URL("tmp.wasm", import.meta.url));
export default module;
`;
if (args.path.endsWith('.ts')) {
if (portable) {
const contents = await Bun.file(args.path).text();
return {
contents: `import "assemblyscript/std/portable/index.js";\n${contents}`,
loader: 'tsx',
}
}
const jsPath = wasmPath.replace(/\.wasm$/, '.js');
const ascArgs = [
args.path,
'--outFile', wasmPath,
'--bindings', 'esm',
'-Ospeed'
];
const { error, stderr } = await asc.main(ascArgs);
if (error) {
console.error(stderr.toString(), error.message);
throw error;
}
jsContent = await Bun.file(jsPath).text();
} else if (args.path.endsWith('.wasm')) {
wasmPath = args.path;
} else {
const result = await $`clang --target=wasm32 -O3 --no-standard-libraries -Wl,--export-all -Wl,--no-entry -fno-builtin -o ${wasmPath} ${args.path}`;
if (result.exitCode !== 0) {
throw new Error('Compile failed, check output');
}
}
const wasmPath = path.resolve(import.meta.dir, '..', '..', 'dist', 'tmp.wasm');
const jsPath = wasmPath.replace(/\.wasm$/, '.js');
const ascArgs = [
args.path,
'--outFile', wasmPath,
'--bindings', 'esm',
];
ascArgs.push(production ? '--optimize' : '--debug');
const { error, stderr } = await asc.main(ascArgs);
const wasmContent = await Bun.file(wasmPath).arrayBuffer();
const wasmBuffer = Buffer.from(wasmContent).toString('base64');
const wasmURL = `data:application/wasm;base64,${wasmBuffer}`;
if (error) {
console.error(stderr.toString(), error.message);
throw error;
} else {
const jsContent = await Bun.file(jsPath).text();
const wasmContent = await Bun.file(wasmPath).arrayBuffer();
const wasmBuffer = Buffer.from(wasmContent).toString('base64');
const wasmURL = `data:application/wasm;base64,${wasmBuffer}`;
return {
loader: 'js',
contents: jsContent
.replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`),
};
}
return {
loader: 'js',
contents: jsContent.replace(/new URL\([^)]*\)/, `new URL(${JSON.stringify(wasmURL)})`),
};
});
}
};

47
src/games/life/index.ts Normal file
View File

@ -0,0 +1,47 @@
import life from "./life.c";
const width = life.getWidth();
const height = life.getHeight();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const imageData = context.createImageData(width, height);
const initField = life.initField as CallableFunction;
const step = life.step as CallableFunction;
const pixels = new Uint8Array(life.memory.buffer, life.getPixels(), width * height * 4);
export default function main() {
document.body.append(canvas);
initField(Date.now());
canvas.width = width;
canvas.height = height;
canvas.style.height = '100%';
canvas.style.imageRendering = 'pixelated';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
console.log(life, pixels.length);
requestAnimationFrame(loop);
}
let sum = 0;
let count = 0;
async function loop() {
const start = performance.now();
step();
const end = performance.now();
sum += end - start;
count++;
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
context.clearRect(0, 0, 35, 15);
context.fillText(`${(sum / count).toFixed(1)} ms`, 2, 10);
requestAnimationFrame(loop);
}

103
src/games/life/life.c Normal file
View File

@ -0,0 +1,103 @@
#include <stdint.h>
#define width 512
#define height 512
static uint8_t field[width * height];
static uint8_t nextField[width * height];
static uint8_t pixels[width * height * 4];
void* malloc(uint32_t n);
static uint8_t rand8(void);
static int countNeighbours(int x, int y);
int getWidth() { return width; }
int getHeight() { return height; }
uint8_t* getPixels() { return pixels; }
void step()
{
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int count = countNeighbours(x, y);
uint8_t currentCell = field[x + y * width];
int nextCell = currentCell;
if (currentCell && count < 2) nextCell = 0;
else if (currentCell && count > 3) nextCell = 0;
else if (!currentCell && count == 3) nextCell = 1;
nextField[x + y * width] = nextCell;
}
}
for (int i = 0; i < width * height; i++) {
field[i] = nextField[i];
uint8_t px = field[i];
if (px) {
pixels[(i << 2) + 0] = 0;
pixels[(i << 2) + 1] = 0;
pixels[(i << 2) + 2] = 0;
} else {
pixels[(i << 2) + 0] = 255;
pixels[(i << 2) + 1] = 255;
pixels[(i << 2) + 2] = 255;
}
pixels[(i << 2) + 3] = 255;
}
}
static int countNeighbours(int x, int y)
{
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
int xx = (x + i + width) % width;
int yy = (y + j + height) % height;
if (field[xx + yy * width]) {
count++;
}
}
}
return count;
}
/** Random generator implementation from https://stackoverflow.com/a/16761955 */
#define STATE_BYTES 7
#define MULT 0x13B /* for STATE_BYTES==6 only */
#define MULT_LO (MULT & 255)
#define MULT_HI (MULT & 256)
static uint8_t rand_state[STATE_BYTES] = { 0x87, 0xdd, 0xdc, 0x10, 0x35, 0xbc, 0x5c };
static uint8_t rand8(void)
{
static uint16_t c = 0x42;
static int i = 0;
uint16_t t;
uint8_t x;
x = rand_state[i];
t = (uint16_t)x * MULT_LO + c;
c = t >> 8;
#if MULT_HI
c += x;
#endif
x = t & 255;
rand_state[i] = x;
if (++i >= sizeof(rand_state))
i = 0;
return x;
}
void initField(uint32_t randomSeed)
{
*((uint32_t*)&rand_state[STATE_BYTES - sizeof(uint32_t)]) = randomSeed; // Voodoo
for (int i = 0; i < width * height; i++) {
field[i] = rand8() & 1;
}
}

27
src/types.d.ts vendored
View File

@ -34,4 +34,31 @@ declare module "*.mp3" {
declare module "*.ogg" {
const audio: HTMLAudioElement;
export default audio;
}
declare module "*.wasm" {
const instance: {
memory: WebAssembly.Memory;
data: DataView;
[x: string]: (...args: any) => any;
};
export default instance;
}
declare module "*.c" {
const instance: {
memory: WebAssembly.Memory;
data: DataView;
[x: string]: (...args: any) => any;
};
export default instance;
}
declare module "*.cpp" {
const instance: {
memory: WebAssembly.Memory;
data: DataView;
[x: string]: (...args: any) => any;
};
export default instance;
}