diff --git a/.gitignore b/.gitignore index c2b8d7c..2999fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ node_modules/ -bun.lockb \ No newline at end of file +bun.lockb +bun.lock + +# Generated files +@types/ +build/ \ No newline at end of file diff --git a/README.md b/README.md index 279ff21..801c8e4 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,28 @@ and, more... +## Platform Support + +hyperimport works on **Linux, macOS, and Windows**. + +### Windows Requirements + +On Windows, you need to install [LLVM](https://llvm.org/) to use hyperimport: + +```bash +# Using scoop (recommended) +scoop install llvm + +# Or using chocolatey +choco install llvm + +# Or download from https://releases.llvm.org/ +``` + +The `llvm-nm` tool is used to extract symbols from compiled libraries on Windows. + + + ## Documentation *—"I wanna learn more about this! How do I get started?"* diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..5489a08 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,7 @@ +[hyperimport] +loaders = ["rs", "zig"] + +[test] +preload = ["./preload.ts"] +root = "./test" + diff --git a/package.json b/package.json index 658e115..eb7846a 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.2.0", "description": "⚡ Import c, rust, zig etc. files in your TypeScript code and more.", "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, "files": [ "preload.ts", "src" @@ -27,6 +30,9 @@ }, "homepage": "https://github.com/tr1ckydev/hyperimport#readme", "bin": "./src/cli.ts", + "dependencies": { + "web-tree-sitter": "0.25.10" + }, "devDependencies": { "bun-types": "latest" } diff --git a/preload.ts b/preload.ts index a8e14fe..fe3eb86 100644 --- a/preload.ts +++ b/preload.ts @@ -9,7 +9,9 @@ if (Bun.env.DISABLE_PRELOAD !== "1") { debugLog(config.debug, 3, "registering loaders..."); for (const loader of config.loaders ?? []) { + // Try both .ts file and directory with index.ts await importPlugin(`./src/loaders/${loader}.ts`) + .catch(() => importPlugin(`./src/loaders/${loader}/index.ts`)) .then(name => debugLog(config.debug, 2, name, "has been registered")) .catch(() => debugLog(config.debug, 1, "loader not found:", loader)); } @@ -35,7 +37,7 @@ if (Bun.env.DISABLE_PRELOAD !== "1") { async function importPlugin(path: string) { const l = await import(path); - const plugin = await new l.default(config.debug, cwd).toPlugin(); + const plugin = await new l.default().toPlugin(); Bun.plugin(plugin); return plugin.name; } diff --git a/src/loader.ts b/src/loader.ts index ae4e51c..d0e14f6 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,9 +1,9 @@ import { BunPlugin } from "bun"; -import { FFIFunction, Narrow, dlopen, suffix } from "bun:ffi"; +import { FFIFunction, dlopen, suffix } from "bun:ffi"; import { mkdirSync, readFileSync } from "fs"; import { basename, parse } from "path"; import { LoaderConfig } from "./types"; -import { lastModified, nm } from "./utils"; +import { lastModified } from "./utils"; export default class { /**The name of the loader. */ @@ -31,6 +31,13 @@ export default class { * By default asks for the build command and output directory from the user on importing the source file for the first time. */ async initConfigPre() { + // In test mode, automatically use defaults without prompting + if (process.env.NODE_ENV === "test") { + console.log(`\x1b[33m[HYPERIMPORT]\x1b[39m: ${this.name}\nNo configuration was found for "${this.config.importPath}"\nUsing default configuration (test mode)...\n`); + mkdirSync(this.config.outDir, { recursive: true }); + return; + } + console.log(`\x1b[33m[HYPERIMPORT]\x1b[39m: ${this.name}\nNo configuration was found for "${this.config.importPath}"\nEnter the build command and output directory to configure it.\nPress enter to use the default values.\n`); this.config.buildCommand = prompt("build command: (default)")?.split(" ") ?? this.config.buildCommand; this.config.outDir = prompt(`output directory: (${this.config.outDir})`) ?? this.config.outDir; @@ -42,20 +49,35 @@ export default class { */ async initConfigTypes() { const filename = basename(this.config.importPath); - mkdirSync(`${this.cwd}/@types/${filename}`, { recursive: true }); - Bun.write(`${this.cwd}/@types/${filename}/lastModified`, lastModified(this.config.importPath)); - const configWriter = Bun.file(`${this.cwd}/@types/${filename}/config.ts`).writer(); - configWriter.write(`import { LoaderConfig, T } from "hyperimport";\nexport default {\n\tbuildCommand: ${JSON.stringify(this.config.buildCommand)},\n\toutDir: "${this.config.outDir}",\n\tsymbols: {`); - for (const symbol of nm(this.config.libPath)) { - configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [],\n\t\t\treturns: T.void\n\t\t},`); + const configDir = `${this.cwd}/@types/${filename}`; + mkdirSync(configDir, { recursive: true }); + Bun.write(`${configDir}/lastModified`, lastModified(this.config.importPath)); + + const configWriter = Bun.file(`${configDir}/config.ts`).writer(); + configWriter.write(`import type { LoaderConfig } from "hyperimport";\nimport { T } from "hyperimport";\nexport default {\n\tbuildCommand: ${JSON.stringify(this.config.buildCommand)},\n\toutDir: "${this.config.outDir}",\n\tsymbols: {`); + + const types = this._config.parseTypes ? await this._config.parseTypes(this.config.importPath) : undefined; + + if (types && Object.keys(types).length > 0) { + for (const [symbol, type] of Object.entries(types)) { + const args = type.args.join(", "); + if (type.line) { + // Convert Windows backslashes to forward slashes for proper file:// URL + const filePath = this.config.importPath.replace(/\\/g, '/'); + configWriter.write(`\n\t\t/** Source: {@link file:///${filePath}#L${type.line}} */`); + } + configWriter.write(`\n\t\t${symbol}: {\n\t\t\targs: [${args}],\n\t\t\treturns: ${type.returns}\n\t\t},`); + } } configWriter.write(`\n\t}\n} satisfies LoaderConfig.Main;`); - configWriter.end(); - Bun.write( + await configWriter.end(); + + // Generate types.d.ts - simplified since JSDoc is in config.ts + await Bun.write( `${this.cwd}/@types/${filename}/types.d.ts`, `declare module "*/${filename}" {\n\tconst symbols: import("bun:ffi").ConvertFns;\n\texport = symbols;\n}` ); - console.log(`\n\x1b[32mConfig file has been generated at "${this.cwd}/@types/${filename}/config.ts"\x1b[39m\nEdit the config.ts and set the argument and return types, then rerun the script.`); + console.log(`\n\x1b[32mConfig file has been generated at "${this.cwd}/@types/${filename}/config.ts"\x1b[39m\nTypes have been automatically generated!`); } /** @@ -77,22 +99,24 @@ export default class { const lmfile = `${this.cwd}/@types/${basename(this.config.importPath)}/lastModified`; if (lm !== readFileSync(lmfile).toString()) { await this.build(); + await this.initConfigTypes(); Bun.write(lmfile, lm); } } /** * Imports the symbols defined in `config.ts` to be used when opening the shared library. - * If `config.ts` isn't found, the source file isn't configured yet, hence executes `initConfig()` and exits the process. + * If `config.ts` doesn't exist, generates it automatically with type inference. * @returns An object containing the symbols. */ - async getSymbols(): Promise>> { + async getSymbols(): Promise> { try { await this.ifSourceModify(); return (await import(`${this.cwd}/@types/${basename(this.config.importPath)}/config.ts`)).default.symbols; } catch { await this.initConfig(); - process.exit(); + // Config generated, now import and return it + return (await import(`${this.cwd}/@types/${basename(this.config.importPath)}/config.ts`)).default.symbols; } } @@ -102,7 +126,8 @@ export default class { async preload() { this.config.outDir = this._config.outDir!(this.config.importPath); this.config.buildCommand = this._config.buildCommand!(this.config.importPath, this.config.outDir); - this.config.libPath = `${this.config.outDir}/lib${parse(this.config.importPath).name}.${suffix}`; + const libPrefix = process.platform === "win32" ? "" : "lib"; + this.config.libPath = `${this.config.outDir}/${libPrefix}${parse(this.config.importPath).name}.${suffix}`; } /** diff --git a/src/loaders/library.ts b/src/loaders/library.ts index 9c1bdcf..e5e21b3 100644 --- a/src/loaders/library.ts +++ b/src/loaders/library.ts @@ -3,11 +3,11 @@ import { basename } from "path"; import Loader from "../loader"; import { lastModified, nm } from "../utils"; -export default class extends Loader { +export default class LibraryLoader extends Loader { constructor() { super("Library Loader", { - extension: "so|dylib", + extension: "so|dylib|dll", } ); } diff --git a/src/loaders/rs.ts b/src/loaders/rs/index.ts similarity index 58% rename from src/loaders/rs.ts rename to src/loaders/rs/index.ts index d7b82a0..53a104d 100644 --- a/src/loaders/rs.ts +++ b/src/loaders/rs/index.ts @@ -1,7 +1,8 @@ import { basename } from "path"; -import Loader from "../loader"; +import Loader from "../../loader"; +import { parseRustTypes } from "./parse-types"; -export default class extends Loader { +export default class RustLoader extends Loader { constructor() { super("Rust Loader", { @@ -14,8 +15,12 @@ export default class extends Loader { "--out-dir", outDir ], - outDir: importPath => `build/${basename(importPath)}` + outDir: importPath => `build/${basename(importPath)}`, + parseTypes: async (importPath) => { + const sourceCode = await Bun.file(importPath).text(); + return parseRustTypes(sourceCode); + } } ); } -} \ No newline at end of file +} diff --git a/src/loaders/rs/parse-types.ts b/src/loaders/rs/parse-types.ts new file mode 100644 index 0000000..e1f55d6 --- /dev/null +++ b/src/loaders/rs/parse-types.ts @@ -0,0 +1,168 @@ +import { Language, Parser } from "web-tree-sitter"; + +// WASM URL for tree-sitter Rust parser +const RUST_WASM_URL = "https://github.com/tree-sitter/tree-sitter-rust/releases/download/v0.24.0/tree-sitter-rust.wasm"; + +// Architecture-specific type mapping +const ARCH_SIZE = process.arch === "x64" || process.arch === "arm64" ? "64" : "32"; + +// Rust type to FFI type mapping +const TYPE_MAP: Record = { + "()": "T.void", + "bool": "T.bool", + "u8": "T.u8", + "u16": "T.u16", + "u32": "T.u32", + "u64": "T.u64", + "i8": "T.i8", + "i16": "T.i16", + "i32": "T.i32", + "i64": "T.i64", + "f32": "T.f32", + "f64": "T.f64", + "usize": `T.u${ARCH_SIZE}`, + "isize": `T.i${ARCH_SIZE}`, + "char": "T.u32", +}; + +function mapType(rustType: string): string { + return TYPE_MAP[rustType] || "T.ptr"; +} + +let parserInstance: Parser | null = null; + +// Lazy initialization pattern - parser is created only when needed +async function getParser(): Promise { + if (parserInstance) { + return parserInstance; + } + + // Initialize tree-sitter WASM + await Parser.init(); + + // Fetch and load Rust language WASM + const response = await fetch(RUST_WASM_URL); + const wasmBinary = await response.arrayBuffer(); + const rustLanguage = await Language.load(new Uint8Array(wasmBinary)); + + // Create and configure parser + parserInstance = new Parser(); + parserInstance.setLanguage(rustLanguage); + + return parserInstance; +} + +export interface FunctionType { + args: string[]; + returns: string; + line?: number; +} + +// Parse Rust source code to extract FFI function signatures +// Extracts all #[no_mangle] extern "C" functions +export async function parseRustTypes( + sourceCode: string +): Promise> { + const parser = await getParser(); + const tree = parser.parse(sourceCode); + + if (!tree) { + return {}; + } + + const types: Record = {}; + + // Traverse all function items in the AST + for (let i = 0; i < tree.rootNode.childCount; i++) { + const node = tree.rootNode.child(i); + if (!node || node.type !== "function_item") { + continue; + } + + // Check if function has #[no_mangle] attribute (required for FFI export) + const prevSibling = i > 0 ? tree.rootNode.child(i - 1) : null; + const hasNoMangle = prevSibling?.type === "attribute_item" && + prevSibling.text.includes("no_mangle"); + + if (!hasNoMangle) { + continue; + } + + // Get function name + const fnNameNode = node.children.find(child => child?.type === "identifier"); + if (!fnNameNode) { + continue; + } + + const fnName = fnNameNode.text; + + // Parse parameters + const parametersNode = node.children.find(child => child?.type === "parameters"); + const args: string[] = []; + + if (parametersNode) { + for (const paramChild of parametersNode.children) { + if (!paramChild || paramChild.type !== "parameter") { + continue; + } + + // Check for primitive types + const primitiveType = paramChild.children.find(child => child?.type === "primitive_type"); + if (primitiveType) { + args.push(mapType(primitiveType.text)); + continue; + } + + // Check for pointer types + const pointerType = paramChild.children.find(child => child?.type === "pointer_type"); + if (pointerType) { + args.push("T.ptr"); + continue; + } + + // Check for reference types + const referenceType = paramChild.children.find(child => child?.type === "reference_type"); + if (referenceType) { + args.push("T.ptr"); + continue; + } + + // Check for function types (callbacks) + const functionType = paramChild.children.find(child => child?.type === "function_type"); + if (functionType) { + args.push("T.function"); + continue; + } + + // Default to pointer for unknown types + args.push("T.ptr"); + } + } + + // Parse return type + let returnType = "T.void"; + const primitiveReturn = node.children.find(child => child?.type === "primitive_type"); + if (primitiveReturn) { + returnType = mapType(primitiveReturn.text); + } else { + const unitReturn = node.children.find(child => child?.type === "unit_type"); + if (unitReturn) { + returnType = "T.void"; + } else { + // Check for pointer return types + const pointerReturn = node.children.find(child => child?.type === "pointer_type"); + if (pointerReturn) { + returnType = "T.ptr"; + } + } + } + + types[fnName] = { + args, + returns: returnType, + line: fnNameNode.startPosition.row + 1 + }; + } + + return types; +} diff --git a/src/loaders/zig.ts b/src/loaders/zig.ts deleted file mode 100644 index 63dc4e3..0000000 --- a/src/loaders/zig.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { suffix } from "bun:ffi"; -import { basename, parse } from "path"; -import Loader from "../loader"; - -export default class extends Loader { - constructor() { - super("Zig Loader", - { - extension: "zig", - buildCommand: (importPath, outDir) => [ - "zig", - "build-lib", - importPath, - "-dynamic", - "-OReleaseFast", - `-femit-bin=${outDir}/lib${parse(importPath).name}.${suffix}` - ], - outDir: importPath => `build/${basename(importPath)}` - } - ); - } -} \ No newline at end of file diff --git a/src/loaders/zig/index.ts b/src/loaders/zig/index.ts new file mode 100644 index 0000000..65f4489 --- /dev/null +++ b/src/loaders/zig/index.ts @@ -0,0 +1,30 @@ +import { suffix } from "bun:ffi"; +import { basename, parse } from "path"; +import Loader from "../../loader"; +import { parseZigTypes } from "./parse-types"; + +export default class ZigLoader extends Loader { + constructor() { + super("Zig Loader", + { + extension: "zig", + buildCommand: (importPath, outDir) => { + const libPrefix = process.platform === "win32" ? "" : "lib"; + return [ + "zig", + "build-lib", + importPath, + "-dynamic", + "-OReleaseFast", + `-femit-bin=${outDir}/${libPrefix}${parse(importPath).name}.${suffix}` + ]; + }, + outDir: importPath => `build/${basename(importPath)}`, + parseTypes: async (importPath) => { + const sourceCode = await Bun.file(importPath).text(); + return parseZigTypes(sourceCode); + } + } + ); + } +} \ No newline at end of file diff --git a/src/loaders/zig/parse-types.ts b/src/loaders/zig/parse-types.ts new file mode 100644 index 0000000..14df138 --- /dev/null +++ b/src/loaders/zig/parse-types.ts @@ -0,0 +1,173 @@ +import { Language, Parser } from "web-tree-sitter"; + +// WASM URL for tree-sitter Zig parser +const ZIG_WASM_URL = "https://github.com/tree-sitter-grammars/tree-sitter-zig/releases/download/v1.1.2/tree-sitter-zig.wasm"; + +// Architecture-specific type mapping +const ARCH_SIZE = process.arch === "x64" || process.arch === "arm64" ? "64" : "32"; + +// Zig type to FFI type mapping +const TYPE_MAP: Record = { + "void": "T.void", + "bool": "T.bool", + "u8": "T.u8", + "u16": "T.u16", + "u32": "T.u32", + "u64": "T.u64", + "i8": "T.i8", + "i16": "T.i16", + "i32": "T.i32", + "i64": "T.i64", + "f32": "T.f32", + "f64": "T.f64", + "usize": `T.u${ARCH_SIZE}`, + "isize": `T.i${ARCH_SIZE}`, + "c_char": "T.i8", + "c_int": "T.i32", + "c_uint": "T.u32", +}; + +function mapType(zigType: string): string { + return TYPE_MAP[zigType] || "T.ptr"; +} + +let parserInstance: Parser | null = null; + +// Lazy initialization pattern - parser is created only when needed +async function getParser(): Promise { + if (parserInstance) { + return parserInstance; + } + + // Initialize tree-sitter WASM + await Parser.init(); + + // Fetch and load Zig language WASM + const response = await fetch(ZIG_WASM_URL); + const wasmBinary = await response.arrayBuffer(); + const zigLanguage = await Language.load(new Uint8Array(wasmBinary)); + + // Create and configure parser + parserInstance = new Parser(); + parserInstance.setLanguage(zigLanguage); + + return parserInstance; +} + +export interface FunctionType { + args: string[]; + returns: string; + line?: number; +} + +// Parse Zig source code to extract FFI function signatures +// Extracts all functions marked with `pub export` +export async function parseZigTypes( + sourceCode: string +): Promise> { + const parser = await getParser(); + const tree = parser.parse(sourceCode); + + if (!tree) { + return {}; + } + + const types: Record = {}; + + // Traverse all function declarations in the AST + for (let i = 0; i < tree.rootNode.childCount; i++) { + const node = tree.rootNode.child(i); + if (!node || node.type !== "function_declaration") { + continue; + } + + // Check if function has `pub export` (required for FFI export) + // Look for "pub" and "export" keywords in the function children + const hasPub = node.children.some(child => child?.type === "pub"); + const hasExport = node.children.some(child => child?.type === "export"); + + if (!hasPub || !hasExport) { + continue; + } + + // Get function name (should be after fn keyword) + const fnNameNode = node.children.find(child => child?.type === "identifier"); + if (!fnNameNode) { + continue; + } + + const fnName = fnNameNode.text; + const args: string[] = []; + + // Parse parameters + const paramsNode = node.children.find(child => child?.type === "parameters"); + + if (paramsNode) { + for (const paramChild of paramsNode.children) { + if (!paramChild || paramChild.type !== "parameter") { + continue; + } + + // Get the type node (comes after the colon) + // Look for builtin_type, pointer_type, or identifier (for custom types) + const typeNode = paramChild.children.find(child => + child?.type === "builtin_type" || + child?.type === "pointer_type" + ); + + if (!typeNode) { + // Check for custom type identifiers + const colonIndex = paramChild.children.findIndex(child => child?.type === ":"); + if (colonIndex >= 0 && colonIndex < paramChild.children.length - 1) { + const possibleType = paramChild.children[colonIndex + 1]; + if (possibleType?.type === "identifier") { + args.push("T.ptr"); // Custom types default to ptr + continue; + } + } + args.push("T.ptr"); + continue; + } + + // Handle pointer types + if (typeNode.type === "pointer_type") { + // Check if it's a function pointer (contains "fn") + if (typeNode.text.includes("fn")) { + args.push("T.function"); + } else { + args.push("T.ptr"); + } + continue; + } + + // Handle primitive types + args.push(mapType(typeNode.text)); + } + } + + // Parse return type (comes after parameters, before block) + let returnType = "T.void"; + const returnTypeNode = node.children.find(child => + (child?.type === "builtin_type" || + child?.type === "pointer_type" || + child?.type === "identifier") && + child !== fnNameNode + ); + + if (returnTypeNode) { + if (returnTypeNode.type === "pointer_type") { + returnType = "T.ptr"; + } else { + returnType = mapType(returnTypeNode.text); + } + } + + types[fnName] = { + args, + returns: returnType, + line: fnNameNode.startPosition.row + 1 + }; + } + + return types; +} diff --git a/src/types.ts b/src/types.ts index f0e6f1a..350a29e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { FFIFunction, Narrow } from "bun:ffi"; +import { FFIFunction } from "bun:ffi"; export { FFIType as T } from "bun:ffi"; @@ -14,13 +14,14 @@ export namespace LoaderConfig { export interface Main { buildCommand?: string[], outDir?: string, - symbols: Record>, + symbols: Record, } export interface Builder { extension: string, buildCommand?: (importPath: string, outDir: string) => string[], outDir?: (importPath: string) => string, + parseTypes?: (importPath: string) => Promise | undefined>, } export interface Internal { @@ -28,7 +29,7 @@ export namespace LoaderConfig { libPath: string, buildCommand: string[], outDir: string, - symbols: Record>, + symbols: Record, } } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 53f21c3..d1a8cc5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,5 +22,6 @@ export function lastModified(path: string) { * @param path The path to the library to be loaded. */ export function nm(path: string) { - return [...Bun.spawnSync(["nm", path]).stdout.toString().matchAll(/T (.*)$/gm)].map(x => x[1][0] === "_" ? x[1].slice(1) : x[1]); + const nmCommand = process.platform === "win32" ? "llvm-nm" : "nm"; + return [...Bun.spawnSync([nmCommand, path]).stdout.toString().matchAll(/T (.*)$/gm)].map(x => x[1][0] === "_" ? x[1].slice(1) : x[1]); } diff --git a/test/capture-output.ts b/test/capture-output.ts new file mode 100644 index 0000000..d921971 --- /dev/null +++ b/test/capture-output.ts @@ -0,0 +1,40 @@ +import { dirname } from "path"; + +export async function captureStdout(script: string): Promise { + const proc = Bun.spawn(["bun", "-e", script], { + cwd: dirname(import.meta.dir), // Run from project root where builds are + stdout: "pipe", + stderr: "pipe", + }); + + const stdout = await new Response(proc.stdout).text(); + const stderr = await new Response(proc.stderr).text(); + await proc.exited; + + // If there's any unexpected stderr, throw it + if (stderr.trim()) { + throw new Error(`Unexpected stderr: ${stderr.trim()}`); + } + + return stdout.trim(); +} + +export async function captureStderr(script: string): Promise { + const proc = Bun.spawn(["bun", "-e", script], { + cwd: dirname(import.meta.dir), // Run from project root where builds are + stdout: "pipe", + stderr: "pipe", + }); + + const stdout = await new Response(proc.stdout).text(); + const stderr = await new Response(proc.stderr).text(); + await proc.exited; + + // If there's any unexpected stdout, throw it + if (stdout.trim()) { + throw new Error(`Unexpected stdout: ${stdout.trim()}`); + } + + return stderr.trim(); +} + diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..cb020c7 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,95 @@ +import { CString, JSCallback } from "bun:ffi"; +import { expect, test } from "bun:test"; +import { captureStderr, captureStdout } from "./capture-output"; + +const testCases = [ + { + name: "rust", + lib: "test.rs", + importPath: "./test.rs", + }, + { + name: "zig", + lib: "test.zig", + importPath: "./test.zig", + }, +]; + +for (const { name, lib, importPath } of testCases) { + const { add, sub, mul, not, print_stdout, print_stderr, echo, call } = await import(importPath); + + test(`${name}: add function`, () => { + expect(add(3, 2)).toBe(5); + }); + + test(`${name}: sub function`, () => { + expect(sub(3, 2)).toBe(1); + }); + + test(`${name}: mul function`, () => { + expect(mul(3.2, 2.1)).toBeCloseTo(6.72, 1); + }); + + test(`${name}: not function`, () => { + expect(not(true)).toBe(false); + expect(not(false)).toBe(true); + }); + + test(`${name}: print to stdout`, async () => { + const libPrefix = process.platform === "win32" ? "" : "lib"; + const libExt = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; + + const output = await captureStdout(` + import { dlopen, FFIType } from "bun:ffi"; + const { symbols } = dlopen("./build/${lib}/${libPrefix}test.${libExt}", { + print_stdout: { args: [], returns: FFIType.void } + }); + symbols.print_stdout(); + `); + + expect(output).toBe("stdout message"); + }); + + test(`${name}: print to stderr`, async () => { + const libPrefix = process.platform === "win32" ? "" : "lib"; + const libExt = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; + + const output = await captureStderr(` + import { dlopen, FFIType } from "bun:ffi"; + const { symbols } = dlopen("./build/${lib}/${libPrefix}test.${libExt}", { + print_stderr: { args: [], returns: FFIType.void } + }); + symbols.print_stderr(); + `); + + expect(output).toBe("stderr message"); + }); + + test(`${name}: echo function (pointer parameter)`, async () => { + const libPrefix = process.platform === "win32" ? "" : "lib"; + const libExt = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; + + const output = await captureStdout(` + import { dlopen, FFIType, ptr } from "bun:ffi"; + const { symbols } = dlopen("./build/${lib}/${libPrefix}test.${libExt}", { + echo: { args: [FFIType.ptr], returns: FFIType.void } + }); + symbols.echo(ptr(Buffer.from("hello from JS\\0", "utf-8"))); + `); + + expect(output).toBe("hello from JS"); + }); + + test(`${name}: call function with callback`, () => { + const callback = new JSCallback( + (ptr, length) => /Hello/.test(new CString(ptr, length).toString()), + { + returns: "bool", + args: ["ptr", "usize"] + } + ); + + expect(call(callback)).toBe(true); + }); +} + diff --git a/test/test.rs b/test/test.rs new file mode 100644 index 0000000..4b004b1 --- /dev/null +++ b/test/test.rs @@ -0,0 +1,46 @@ +use std::os::raw::c_char; + +#[no_mangle] +pub extern "C" fn add(a: u32, b: u32) -> u32 { + a + b +} + +#[no_mangle] +pub extern "C" fn sub(a: i32, b: i32) -> i32 { + a - b +} + +#[no_mangle] +pub extern "C" fn mul(a: f32, b: f32) -> f32 { + a * b +} + +#[no_mangle] +pub extern "C" fn not(a: bool) -> bool { + !a +} + +#[no_mangle] +pub extern "C" fn print_stdout() -> () { + println!("stdout message"); +} + +#[no_mangle] +pub extern "C" fn print_stderr() -> () { + eprintln!("stderr message"); +} + +#[no_mangle] +pub extern "C" fn echo(c_string: *const c_char) -> () { + let rust_str = unsafe { + std::ffi::CStr::from_ptr(c_string).to_str().unwrap() + }; + println!("{}", rust_str); +} + +#[no_mangle] +pub extern "C" fn call(callback: extern fn(*const u8, usize) -> bool) -> bool { + const string: &str = "Hello, world!"; + callback(string.as_ptr(), string.len()) +} + diff --git a/test/test.zig b/test/test.zig new file mode 100644 index 0000000..ee3530c --- /dev/null +++ b/test/test.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub export fn add(a: i32, b: i32) i32 { + return a + b; +} + +pub export fn sub(a: i32, b: i32) i32 { + return a - b; +} + +pub export fn mul(a: f32, b: f32) f32 { + return a * b; +} + +pub export fn not(a: bool) bool { + return !a; +} + +pub export fn print_stdout() void { + const stdout = std.io.getStdOut().writer(); + stdout.print("stdout message\n", .{}) catch unreachable; +} + +pub export fn print_stderr() void { + std.debug.print("stderr message\n", .{}); +} + +pub export fn echo(c_string: [*:0]const u8) void { + const stdout = std.io.getStdOut().writer(); + stdout.print("{s}\n", .{c_string}) catch unreachable; +} + +pub export fn call(callback: *const fn ([*]const u8, usize) callconv(.C) bool) bool { + const string = "Hello, world!"; + return callback(string.ptr, string.len); +} + diff --git a/tsconfig.json b/tsconfig.json index 7556e1d..d310bb6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,6 @@ "types": [ "bun-types" // add Bun global ] - } + }, + "include": ["src/**/*", "test/**/*", "preload.ts"] }