Skip to content

Commit

Permalink
node:ffi, new helpers and options (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
xan105 authored Nov 30, 2023
1 parent a9b3d6a commit b3edaa2
Show file tree
Hide file tree
Showing 26 changed files with 979 additions and 296 deletions.
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

0 comments on commit b3edaa2

Please sign in to comment.