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
4 changes: 0 additions & 4 deletions .npmignore

This file was deleted.

138 changes: 118 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
About
=====

Foreign Function Interface helper. Provides a friendly abstraction/API for:
Foreign Function Interface (FFI) helper. Provides a friendly abstraction/API for:

- [ffi-napi](https://www.npmjs.com/package/ffi-napi) (MIT)
- [koffi](https://www.npmjs.com/package/koffi) (MIT)
Expand Down Expand Up @@ -101,6 +101,11 @@ npm install @xan105/ffi
Please note that `ffi-napi` and `koffi` are optional peer dependencies.<br />
Install the one you wish to use yourself (or both 🙃).

### ⚛️ Electron

⚠️ NB: As of this writing `ffi-napi` does not work with Electron >= 21.x.<br />
Due to [Electron and the V8 Memory Cage](https://www.electronjs.org/blog/v8-memory-cage).

API
===

Expand All @@ -120,24 +125,29 @@ import ... from "@xan105/ffi/koffi";

Load the given library path and return an handle function to call library's symbol(s).

**Option**
⚙️ **Option**

- `ignoreLoadingFail?: boolean` (false)

Silent fail if the given library couldn't be loaded.<br />
💡 Handle will return `undefined` in that case.
When set to `true` the handle function will silently fail if the given library couldn't be loaded and return `undefined` in such case.

- `ignoreMissingSymbol?: boolean` (false)

Silent fail if the given library doesn't have the called symbol.<br />
💡 Handle will return `undefined` in that case.
When set to `true` the handle function will silently fail if the given library doesn't have the called symbol and return `undefined` in such case.

- `lazy` (false)

- `abi?: string` ("func" for koffi and "default_abi" for ffi-napi)
When set to `true` use `RTLD_LAZY` (lazy-binding) on POSIX platforms otherwise use `RTLD_NOW`.

ABI convention to use. Use this when you need to ex: winapi x86 requires "stdcall".
- `abi?: string` (koffi: "func" | ffi-napi: "default_abi")

ABI convention to use. Use this when you need to.<br />
_ex: winapi x86 requires "stdcall"._

**Return**

An handle function to call library's symbol(s).

```ts
function(symbol: string | number, result: unknown, parameters: unknown[]): unknown
```
Expand All @@ -153,21 +163,20 @@ See the corresponding FFI library for more information on what to pass for `resu
```js
import { load } from "@xan105/ffi/[ napi | koffi ]";
const lib = load("libm");
const ceil = lib("ceil", "double", ["double"])
const ceil = lib("ceil", "double", ["double"]);
ceil(1.5); //2
```

#### `dlopen(path: string, symbols: object, option?: object): object`

Open library and define exported symbols. This is a friendly wrapper to `load()` inspired by Deno FFI `dlopen` syntax.

Open library and define exported symbols. This is a friendly wrapper to `load()` inspired by Deno FFI `dlopen` syntax.<br />
If you ever use ffi-napi `ffi.Library()` this will be familiar.

**Param**

- `path: string`

Library path to load
Library path to load.

- `symbols: object`

Expand All @@ -176,28 +185,50 @@ If you ever use ffi-napi `ffi.Library()` this will be familiar.
```ts
{
name: {
symbol?: string | number,
result?: unknown,
parameters?: unknown[],
nonblocking?: boolean,
symbol?: string | number
stub?: boolean
},
...
}
```

By default the property `name` is used for `symbol`. Use `symbol` if you are using a symbol name different than the given property name or if you want to call by ordinal (Koffi).

By default the property name is used for `symbol` when omitted. Use `symbol` if you are using a different name than the symbol name or if you want to call by ordinal (Koffi).
`result` and `parameters` are the same as for the returned handle from `load()`.<br />
If omitted, `result` is set to "void" and `parameters` to an empty array.<br />
See the corresponding FFI library for more information on what to pass for `result` and `parameters` as they have string type parser, structure/array/pointer interface, ... and other features.

When `nonblocking` is `true` (default false) this will return the promisified `async()` method of the corresponding symbol (see corresponding ffi library asynchronous calling). The rest is the same as for `load()`.
When `nonblocking` is `true` the corresponding symbol will return the promisified `async()` method (asynchronous calling). 💡 If set, this superseed the _"global"_ `nonblocking` option (see below).

- option?: object
When `stub` is `true` the corresponding symbol will return a no-op if its missing.<br />
💡 If set, this superseed the _"global"_ `stub` option (see below).

- ⚙️ `option?: object`

Pass option(s) to `load()`. See above.
Same as `load()` (see above) in addition to the following:

+ `errorAtRuntime?: boolean` (false)

When set to `true`, initialisation error will be thrown on symbol invocation.

+ `nonblocking?: boolean` (false)

When set to `true`, every symbols will return the corresponding promisified `async()` method (asynchronous calling).<br />
💡 This can be overriden per symbol (see symbol definition above).

+ `stub?: boolean` (false)

When set to `true`, every missing symbols will return a no-op.<br />
💡 This can be overriden per symbol (see symbol definition above).

**Return**

An object with the given symbol(s) as properties.

❌ Throws on error
❌ Throws on error.

**Example**

Expand Down Expand Up @@ -348,9 +379,54 @@ library.doSomething();
callback.close();
```

#### `pointer(value: unknown, direction?: string): any`
#### `pointer(value: unknown, direction?: string): unknown`

Just a shorthand to `ref.refType(x)` (ffi-napi) and `koffi.out/inout(koffi.pointer(x))` (koffi) to define a pointer.
Just a shorthand to define a pointer.

```js
import { dlopen, types, pointer } from "@xan105/ffi/[ napi | koffi ]";

const dylib = dlopen("shell32.dll", {
SHQueryUserNotificationState: {
result: types.win32.HRESULT,
parameters: [
pointer(types.win32.ENUM, "out")
]
}
}, { abi: "stdcall" });
```

#### `struct(schema: unknown): unknown`

Just a shorthand to define a structure.

```js
import { dlopen, types, struct, pointer } from "@xan105/ffi/[ napi | koffi ]";

const POINT = struct({ //define struct
x: types.win32.LONG,
y: types.win32.LONG
});

const dylib = dlopen("user32.dll", { //lib loading
GetCursorPos: {
result: types.win32.BOOL,
parameters: [ pointer(POINT, "out") ] //struct pointer
}
}, { abi: "stdcall" });

//⚠️ NB: Struct are use differently afterwards:

//Koffi
const cursorPos = {};
GetCursorPos(cursorPos);
console.log(cursorPos) //{ x: 0, y: 0 }

//ffi-napi
const cursorPos = new POINT();
getCursorPos(cursorPos.ref());
console.log({ x: cursorPos.x, y: cursorPos.y });
```

#### `alloc(type: unknown): { pointer: Buffer, get: ()=> unknown }`

Expand All @@ -363,4 +439,26 @@ const dylib = dlopen(...); //lib loading
const number = alloc("int"); //allocate Buffer for the output data
dylib.manipulate_number(number.pointer);
const result = number.get();
```

#### `lastError(option?: object): string[] | number`

Shorthand to errno (POSIX) and GetLastError (win32).

⚙️ **Option**

- `translate?: boolean` (true)

When an error code is known it will be 'translated' to its corresponding message and code values as<br /> `[message: string, code?: string]`. If you only want the raw numerical code set it to `false`.

eg:
```js
if(result !== 0){ //something went wrong

console.log(lastError())
//['No such file or directory', 'ENOENT']

console.log(lastError({ translate: false }));
// 2
}
```
29 changes: 25 additions & 4 deletions lib/ffi-napi/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ found in the LICENSE file in the root directory of this source tree.
import process from "node:process";
import ffi from "ffi-napi";
import ref from "ref-napi";
import ref_struct from "ref-struct-di";
import { shouldObj } from "@xan105/is/assert";
import { asArray } from "@xan105/is/opt";
import { asArray, asBoolean } from "@xan105/is/opt";
import { isWindows } from "@xan105/is";
import { errorLookup } from "@xan105/error";
import { GetLastError } from "./util/win32.js";

const StructType = ref_struct(ref);

class Callback{

Expand Down Expand Up @@ -63,14 +69,29 @@ function pointer(value){
return ref.refType(value);
}

function struct(schema){
return StructType(schema);
}

function alloc(type){
const buff = Object.assign(Object.create(null), {
return Object.assign(Object.create(null), {
pointer: ref.alloc(type),
get: function(){
return this.pointer.deref();
}
});
return buff;
}

export { Callback, pointer, alloc };
function lastError(option = {}){
const errno = isWindows() ? GetLastError() : ffi.errno();
const options = { translate: asBoolean(option?.translate) ?? true };
return options.translate ? errorLookup(errno) : errno;
}

export {
Callback,
pointer,
struct,
alloc,
lastError
};
Loading