bun-polyfills: refactor ffi module

This commit is contained in:
jhmaster2000
2023-10-13 17:45:27 -03:00
parent 91f4ba534b
commit 8fcd645bae
3 changed files with 243 additions and 235 deletions

View File

@@ -40,7 +40,7 @@ export const main = path.resolve(process.cwd(), process.argv[1] ?? 'repl') satis
//? These are automatically updated on build by tools/updateversions.ts, do not edit manually.
export const version = '1.0.4' satisfies typeof Bun.version;
export const revision = 'a25bb42416fffaeef837b592bf81cad8b257aa48' satisfies typeof Bun.revision;
export const revision = '91f4ba534be9e23e6d4543738301af393848e954' satisfies typeof Bun.revision;
export const gc = (globalThis.gc ? (() => (globalThis.gc!(), process.memoryUsage().heapUsed)) : (() => {
const err = new Error('[bun-polyfills] Garbage collection polyfills are only available when Node.js is ran with the --expose-gc flag.');

View File

@@ -22,12 +22,7 @@ koffi.alias('cstring', 'uint8_t*');
koffi.alias('pointer', 'void*');
koffi.alias('ptr', 'void*');
function bunffiTypeToKoffiType(type: bunffi.FFITypeOrString = 'void'): string {
if (typeof type === 'number') return ffi.FFIType[type];
else return type;
}
enum FFIType {
export enum FFIType {
char = 0,
i8 = 1,
int8_t = 1,
@@ -59,6 +54,12 @@ enum FFIType {
u64_fast = 16,
function = 17,
};
FFIType satisfies typeof bunffi.FFIType;
function bunffiTypeToKoffiType(type: bunffi.FFITypeOrString = 'void'): string {
if (typeof type === 'number') return FFIType[type];
else return type;
}
/**
* Koffi/Node.js don't seem to have a way to get the pointer address of a value, so we have to track them ourselves,
@@ -67,240 +68,247 @@ enum FFIType {
const ptrsToValues = new Map<bunffi.Pointer, unknown>();
let fakePtr = 4;
const ffi = {
dlopen<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(name: string, symbols: Fns) {
const lib = koffi.load(name);
const outsyms = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
const returnType = bunffiTypeToKoffiType(def.returns);
const argTypes = def.args?.map(bunffiTypeToKoffiType) ?? [];
const rawfn = lib.func(
sym,
returnType,
argTypes,
);
Reflect.set(
outsyms,
sym,
function(...args: any[]) {
args.forEach((arg, i) => {
if (typeof arg === 'number' && (argTypes[i] === 'ptr' || argTypes[i] === 'pointer')) {
const ptrVal = ptrsToValues.get(arg);
if (!ptrVal) throw new Error(
`Untracked pointer ${arg} in ffi function call ${sym}, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
args[i] = ptrVal;
}
});
const rawret = rawfn(...args);
if (returnType === 'function' || returnType === 'pointer' || returnType === 'ptr') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return ptrAddr;
export const suffix = (
process.platform === 'darwin' ? '.dylib' :
(process.platform === 'win32' ? '.dll' : '.so')
) satisfies typeof bunffi.suffix;
export const dlopen = (<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(name: string, symbols: Fns) => {
const lib = koffi.load(name);
const outsyms = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
const returnType = bunffiTypeToKoffiType(def.returns);
const argTypes = def.args?.map(bunffiTypeToKoffiType) ?? [];
const rawfn = lib.func(
sym,
returnType,
argTypes,
);
Reflect.set(
outsyms,
sym,
function (...args: any[]) {
args.forEach((arg, i) => {
if (typeof arg === 'number' && (argTypes[i] === 'ptr' || argTypes[i] === 'pointer')) {
const ptrVal = ptrsToValues.get(arg);
if (!ptrVal) throw new Error(
`Untracked pointer ${arg} in ffi function call ${sym}, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
args[i] = ptrVal;
}
if (returnType === 'cstring') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return new ffi.CString(ptrAddr);
}
return rawret;
});
const rawret = rawfn(...args);
if (returnType === 'function' || returnType === 'pointer' || returnType === 'ptr') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return ptrAddr;
}
);
if (returnType === 'cstring') {
const ptrAddr = Number(koffi.address(rawret));
ptrsToValues.set(ptrAddr, rawret);
return new CString(ptrAddr);
}
return rawret;
}
);
}
return {
close() { lib.unload(); },
symbols: outsyms,
};
}) satisfies typeof bunffi.dlopen;
export const linkSymbols = (<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(symbols: Fns) => {
const linked = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
if (!def.ptr) throw new Error('ffi.linkSymbols requires a non-null pointer');
Reflect.set(linked, sym, CFunction(def as typeof def & { ptr: bunffi.Pointer; }));
}
return {
close() { },
symbols: linked,
};
}) satisfies typeof bunffi.linkSymbols;
export const viewSource = ((symsOrCb, isCb) => {
// Impossible to polyfill, but we preserve the important properties of the function:
// 1. Returns string if the 2nd argument is true, or an array of strings if it's false/unset.
// 2. The string array has the same length as there are keys in the given symbols object.
const stub = '/* [native code] */' as const;
return isCb ? stub : Object.keys(symsOrCb).map(() => stub) as any; // any cast to suppress type error due to non-overload syntax
}) satisfies typeof bunffi.viewSource;
export const toBuffer = ((ptr, bOff, bLen) => {
const arraybuffer = toArrayBuffer(ptr, bOff, bLen);
return Buffer.from(arraybuffer);
}) satisfies typeof bunffi.toBuffer;
//! Problem: these arraybuffer views are not mapped to the native memory, so they can't be used to modify the memory.
export const toArrayBuffer = ((ptr, byteOff?, byteLen?) => {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.toArrayBuffer, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return view as ArrayBuffer; // ?
if (util.types.isExternal(view)) {
if (byteLen === undefined) {
let bytes = [], byte, off = 0;
do {
byte = koffi.decode(view, off++, 'unsigned char[]', 1);
bytes.push(byte[0]);
} while (byte[0]);
bytes.pop();
return new Uint8Array(bytes).buffer as ArrayBuffer; // ?
} else {
return koffi.decode(view, byteOff ?? 0, 'unsigned char[]', byteLen).buffer;
}
return {
close() { lib.unload(); },
symbols: outsyms,
};
},
linkSymbols<Fns extends Record<string, bunffi.Narrow<bunffi.FFIFunction>>>(symbols: Fns) {
const linked = {} as bunffi.ConvertFns<typeof symbols>;
for (const [sym, def] of Object.entries(symbols) as [string, bunffi.FFIFunction][]) {
if (!def.ptr) throw new Error('ffi.linkSymbols requires a non-null pointer');
Reflect.set(linked, sym, ffi.CFunction(def as typeof def & { ptr: bunffi.Pointer }));
}
return {
close() {},
symbols: linked,
};
},
viewSource(symsOrCb, isCb) {
// Impossible to polyfill, but we preserve the important properties of the function:
// 1. Returns string if the 2nd argument is true, or an array of strings if it's false/unset.
// 2. The string array has the same length as there are keys in the given symbols object.
const stub = '/* [native code] */' as const;
return isCb ? stub : Object.keys(symsOrCb).map(() => stub) as any; // any cast to suppress type error due to non-overload syntax
},
toBuffer(ptr, bOff, bLen) {
const arraybuffer = this.toArrayBuffer(ptr, bOff, bLen);
return Buffer.from(arraybuffer);
},
//! Problem: these arraybuffer views are not mapped to the native memory, so they can't be used to modify the memory.
toArrayBuffer(ptr, byteOff?, byteLen?) {
}
if (byteOff === undefined) return (view as DataView).buffer;
return (view as DataView).buffer.slice(byteOff, byteOff + (byteLen ?? (view as DataView).byteLength));
}) satisfies typeof bunffi.toArrayBuffer;
export const ptr = ((view, byteOffset = 0) => {
const known = [...ptrsToValues.entries()].find(([_, v]) => v === view);
if (known) return known[0];
const ptr = fakePtr;
fakePtr += (view.byteLength + 3) & ~0x3;
if (!byteOffset) {
ptrsToValues.set(ptr, view);
return ptr;
} else {
const view2 = new DataView(
(view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) ? view : view.buffer,
byteOffset, view.byteLength
);
ptrsToValues.set(ptr + byteOffset, view2);
return ptr + byteOffset;
}
}) satisfies typeof bunffi.ptr;
export const CFunction = ((sym): CallableFunction & { close(): void; } => {
if (!sym.ptr) throw new Error('ffi.CFunction requires a non-null pointer');
const fnName = `anonymous__${sym.ptr.toString(16).replaceAll('.', '_')}`;
const fnSig = koffi.proto(fnName, bunffiTypeToKoffiType(sym.returns), sym.args?.map(bunffiTypeToKoffiType) ?? []);
const fnPtr = ptrsToValues.get(sym.ptr);
if (!fnPtr) throw new Error(
`Untracked pointer ${sym.ptr} in ffi.CFunction, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
const fn = koffi.decode(fnPtr, fnSig);
fn.close = () => { };
return fn;
}) satisfies typeof bunffi.CFunction;
export class CString extends String implements bunffi.CString {
constructor(ptr: bunffi.Pointer, bOff?: number, bLen?: number) {
const buf = toBuffer(ptr, bOff, bLen);
const str = buf.toString('ascii');
super(str);
this.ptr = ptr;
this.#buffer = buf.buffer as ArrayBuffer; // ?
}
close() { };
ptr: bunffi.Pointer;
byteOffset?: number;
byteLength?: number;
#buffer: ArrayBuffer;
get arrayBuffer(): ArrayBuffer { return this.#buffer; };
};
// TODO
export class JSCallback implements bunffi.JSCallback {
constructor(cb: (...args: any[]) => any, def: bunffi.FFIFunction) { }
readonly ptr!: bunffi.Pointer | null;
readonly threadsafe!: boolean;
close() { };
};
export const read = {
f32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.toArrayBuffer, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
`Untracked pointer ${ptr} in ffi.read.f32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return view as ArrayBuffer; // ?
if (util.types.isExternal(view)) {
if (byteLen === undefined) {
let bytes = [], byte, off = 0;
do {
byte = koffi.decode(view, off++, 'unsigned char[]', 1);
bytes.push(byte[0]);
} while (byte[0]);
bytes.pop();
return new Uint8Array(bytes).buffer as ArrayBuffer; // ?
} else {
return koffi.decode(view, byteOff ?? 0, 'unsigned char[]', byteLen).buffer;
}
}
if (byteOff === undefined) return (view as DataView).buffer;
return (view as DataView).buffer.slice(byteOff, byteOff + (byteLen ?? (view as DataView).byteLength));
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat32(bOff, LE);
return koffi.decode(view, bOff, 'f32');
},
ptr(view, byteOffset = 0) {
const known = [...ptrsToValues.entries()].find(([_, v]) => v === view);
if (known) return known[0];
const ptr = fakePtr;
fakePtr += (view.byteLength + 3) & ~0x3;
if (!byteOffset) {
ptrsToValues.set(ptr, view);
return ptr;
} else {
const view2 = new DataView(
(view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) ? view : view.buffer,
byteOffset, view.byteLength
);
ptrsToValues.set(ptr + byteOffset, view2);
return ptr + byteOffset;
}
},
read: {
f32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.f32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat32(bOff, LE);
return koffi.decode(view, bOff, 'f32');
},
f64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.f64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat64(bOff, LE);
return koffi.decode(view, bOff, 'f64');
},
i8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt8(bOff);
return koffi.decode(view, bOff, 'i8');
},
i16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt16(bOff, LE);
return koffi.decode(view, bOff, 'i16');
},
i32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt32(bOff, LE);
return koffi.decode(view, bOff, 'i32');
},
i64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigInt64(bOff, LE);
return koffi.decode(view, bOff, 'i64');
},
intptr(ptr, bOff = 0) {
return this.i32(ptr, bOff);
},
ptr(ptr, bOff = 0) {
const u64 = this.u64(ptr, bOff);
const masked = u64 & 0b11111111_11111111_11111111_11111111_11111111_11111111_00000111_00000000n;
return Number(masked);
},
u8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint8(bOff);
return koffi.decode(view, bOff, 'u8');
},
u16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint16(bOff, LE);
return koffi.decode(view, bOff, 'u16');
},
u32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint32(bOff, LE);
return koffi.decode(view, bOff, 'u32');
},
u64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigUint64(bOff, LE);
return koffi.decode(view, bOff, 'u64');
},
},
suffix:
process.platform === 'darwin' ? '.dylib' :
(process.platform === 'win32' ? '.dll' : '.so'),
CString: class CString extends String implements bunffi.CString {
constructor(ptr: bunffi.Pointer, bOff?: number, bLen?: number) {
const buf = ffi.toBuffer(ptr, bOff, bLen);
const str = buf.toString('ascii');
super(str);
this.ptr = ptr;
this.#buffer = buf.buffer as ArrayBuffer; // ?
}
close() {};
ptr: bunffi.Pointer;
byteOffset?: number;
byteLength?: number;
#buffer: ArrayBuffer;
get arrayBuffer(): ArrayBuffer { return this.#buffer; };
},
CFunction(sym): CallableFunction & { close(): void; } {
if (!sym.ptr) throw new Error('ffi.CFunction requires a non-null pointer');
const fnName = `anonymous__${sym.ptr.toString(16).replaceAll('.', '_')}`
const fnSig = koffi.proto(fnName, bunffiTypeToKoffiType(sym.returns), sym.args?.map(bunffiTypeToKoffiType) ?? []);
const fnPtr = ptrsToValues.get(sym.ptr);
if (!fnPtr) throw new Error(
`Untracked pointer ${sym.ptr} in ffi.CFunction, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
f64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.f64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
const fn = koffi.decode(fnPtr, fnSig);
fn.close = () => {};
return fn;
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getFloat64(bOff, LE);
return koffi.decode(view, bOff, 'f64');
},
// TODO
JSCallback: class JSCallback implements bunffi.JSCallback {
constructor(cb: (...args: any[]) => any, def: bunffi.FFIFunction) {}
readonly ptr!: bunffi.Pointer | null;
readonly threadsafe!: boolean;
close() {};
i8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt8(bOff);
return koffi.decode(view, bOff, 'i8');
},
FFIType,
} satisfies typeof bunffi;
export default ffi;
i16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt16(bOff, LE);
return koffi.decode(view, bOff, 'i16');
},
i32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getInt32(bOff, LE);
return koffi.decode(view, bOff, 'i32');
},
i64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.i64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigInt64(bOff, LE);
return koffi.decode(view, bOff, 'i64');
},
intptr(ptr, bOff = 0) {
return this.i32(ptr, bOff);
},
ptr(ptr, bOff = 0) {
const u64 = this.u64(ptr, bOff);
const masked = u64 & 0b11111111_11111111_11111111_11111111_11111111_11111111_00000111_00000000n;
return Number(masked);
},
u8(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u8, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint8(bOff);
return koffi.decode(view, bOff, 'u8');
},
u16(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u16, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint16(bOff, LE);
return koffi.decode(view, bOff, 'u16');
},
u32(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u32, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getUint32(bOff, LE);
return koffi.decode(view, bOff, 'u32');
},
u64(ptr, bOff = 0) {
const view = ptrsToValues.get(ptr);
if (!view) throw new Error(
`Untracked pointer ${ptr} in ffi.read.u64, this polyfill is limited to pointers obtained through the same instance of the ffi module.`
);
if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) return new DataView(view).getBigUint64(bOff, LE);
return koffi.decode(view, bOff, 'u64');
},
} satisfies typeof bunffi.read;

View File

@@ -26,4 +26,4 @@ globalThis.Bun = bun as typeof bun & {
};
Reflect.set(globalThis, 'jsc', jsc);
Reflect.set(globalThis, 'ffi', ffi.default);
Reflect.set(globalThis, 'ffi', ffi);