Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 7 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,16 @@ bun install @chainsafe/bun-ffi-z
"darwin-arm64",
"win32-x64"
],
"zigCwd": "zig"
"zigCwd": "zig",
"zigExportFiles": [
"src/root.zig"
]
}
```

4. Use bun-ffi-z to select the proper library

```ts
import {openLibrary} from "@chainsafe/bun-ffi-z";

const lib = await openLibrary(
import.meta.dirname,
{
add: {
args: ["u32", "u32"],
returns: "u32"
}
}
)

export const symbols = lib.symbols;
export const close = lib.close;
4. Use bun-ffi-z to generate the bun ffi binding
```bash
bun-ffi-z generate-binding
```

5. Build shared library for native host
Expand Down
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parseArgs } from "node:util";
import { buildCli } from "./build.ts";
import { prepublishCli } from "./prepublish.ts";
import { publish } from "./publish.ts";
import { generateBinding } from "./generateBinding.ts";

export async function cli(): Promise<void> {
const {positionals} = parseArgs({
Expand All @@ -12,6 +13,8 @@ export async function cli(): Promise<void> {
const cmd = positionals[0];

switch (cmd) {
case "generate-binding":
return await generateBinding();
case "build":
return await buildCli();
case "prepublish":
Expand Down
8 changes: 7 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type Config = {
targets: Target[];
optimize?: Optimize;
zigCwd: string;
zigExportFiles?: string[];
};

export interface Json {
Expand All @@ -29,6 +30,7 @@ export function parseConfig(input: Json): Config {
const targets = (input as Record<string, Json>).targets;
const optimize = (input as Record<string, string>).optimize as Optimize;
const zigCwd = (input as Record<string, string>).zigCwd;
const zigExportFiles = (input as Record<string, string>).zigExportFiles;

if (typeof name !== "string") {
throw new Error("Invalid config: expected string \"name\"");
Expand Down Expand Up @@ -64,5 +66,9 @@ export function parseConfig(input: Json): Config {
throw new Error("Invalid config: expected string \"zigCwd\"");
}

return { name, targets, optimize, zigCwd: zigCwd ?? "." };
if (zigExportFiles != null && !Array.isArray(zigExportFiles)) {
throw new Error("Invalid config: expected array \"zigExportFiles\"");
}

return { name, targets, optimize, zigCwd: zigCwd ?? ".", zigExportFiles };
}
45 changes: 45 additions & 0 deletions src/generateBinding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {join} from "node:path";
import { getConfigFromPkgJson, type Json } from "./config.ts";
import { getSymbolsFromZigFiles } from "./getSymbols.ts";

export async function generateBinding(): Promise<void> {
const bunCwd = process.cwd();
const rootPkgJsonPath = join(bunCwd, "package.json");
const pkgJson = await Bun.file(rootPkgJsonPath).json() as Json;
const config = await getConfigFromPkgJson(pkgJson);

const zigExportFiles: string[] = [];
if (config.zigExportFiles == null) {
throw new Error("No zigExportFiles specified in bun-ffi-z config");
}

for (const globStr of config.zigExportFiles) {
const glob = new Bun.Glob(globStr);
for (const file of glob.scanSync(config.zigCwd)) {
const fullPath = join(config.zigCwd, file);
zigExportFiles.push(fullPath);
}
}

const symbols = await getSymbolsFromZigFiles(zigExportFiles);

const output = `
// This file is auto-generated by @chainsafe/bun-ffi-z. Do not edit.

import path from "node:path";
import { openLibrary } from "@chainsafe/bun-ffi-z";

const fns = ${JSON.stringify(symbols, null, 2)};
const lib = await openLibrary(path.join(import.meta.dirname, ".."), fns);

export const binding = lib.symbols;
export const close = lib.close;

`;

const outputPath = join(bunCwd, "src", "binding.ts");
await Bun.write(outputPath, output);

console.log(`Binding generated at ${outputPath}`);
}

103 changes: 103 additions & 0 deletions src/getSymbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Unfortunately, Zig's support for emitting header files is currently broken.
// This functionality is a workaround to generate bun ffi function definitions from parsed zig source files

import type { FFIFunction, FFITypeOrString } from "bun:ffi";

export async function getSymbolsFromZigFiles(files: string[]): Promise<Record<string, FFIFunction>> {
const out: Record<string, FFIFunction> = {};
for (const file of files) {
const content = await Bun.file(file).text();
Object.assign(out, getSymbolsFromZigFileContent(content));
}
return out;
}

export function getSymbolsFromZigFileContent(content: string): Record<string, FFIFunction> {
const lines = content.split("\n");
const out: Record<string, FFIFunction> = {};

for (let i = 0; i < lines.length; i++) {
const line = lines[i]!.trim();

// allow overrides for bun-ffi-z annotations
// looks like:
// bun-ffi-z: myFunction (arg1, arg2) returns
const annotationsLine = line.match(/\/\/ bun-ffi-z:/);
if (annotationsLine) {
const nameStart = line.indexOf(":") + 1;
const nameEnd = line.indexOf("(");

out[line.slice(nameStart, nameEnd).trim()] = {
args: line.slice(nameEnd + 1, line.indexOf(")"))
.split(",")
.map(arg => arg.trim())
.filter(arg => arg) as FFITypeOrString[],
returns: line.slice(line.indexOf(")") + 1).trim() as FFITypeOrString,
};
i++;
continue;
}

const match = line.match(/(?:pub )?export fn (\w+)\(/);
if (!match || match.length < 2) {
continue;
}

const fnName = match[1];

let rawArgsAndReturn = line;
while (!rawArgsAndReturn.includes("{")) {
rawArgsAndReturn += lines[++i]!.trim();
}

const argsStart = rawArgsAndReturn.indexOf("(") + 1;
const argsEnd = rawArgsAndReturn.indexOf(")");

const args = rawArgsAndReturn.slice(argsStart, argsEnd)
.split(",") // split into "name: type"
.map(arg => arg.trim()) // trim each argument
.filter(arg => arg) // filter out empty arguments
.map(arg => arg.split(":")[1]!.trim()) // Get only the type, ignore names
.map(zigTypeToFfiType);

const returnStart = rawArgsAndReturn.indexOf(")") + 1;
const returnEnd = rawArgsAndReturn.indexOf("{");

const returns = zigTypeToFfiType(rawArgsAndReturn.slice(returnStart, returnEnd).trim());

out[fnName] = {
args: args as FFITypeOrString[],
returns: returns as FFITypeOrString,
};
}

return out;
}

export function zigTypeToFfiType(zigType: string): string {
if (zigType.startsWith("*") || zigType.startsWith("?*") || zigType.startsWith("[*c]")) return "ptr";

switch (zigType) {
case "void":
case "bool":
case "u8":
case "i8":
case "u16":
case "i16":
case "u32":
case "i32":
case "u64":
case "i64":
return zigType;
case "c_uint":
return "u32";
case "c_int":
return "i32";
case "size":
return "i64";
case "usize":
return "u64";
}

throw new Error(`Unsupported Zig type: ${zigType}`);
}