Skip to content

Commit f585f25

Browse files
authored
Merge pull request #16 from extism/js-pdk-host-functions
docs: JS-PDK interface description
2 parents 858566e + c3c5a11 commit f585f25

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# JS-PDK Interface Definition
2+
3+
## Purpose
4+
5+
We need to support host functions in the JS PDK but the current way we define
6+
the interface, while working for exports, will not work for imports. Here is a rough
7+
outline of how the current system works:
8+
9+
1. We compile QuickJS to wasm to create the `engine` or `core` module. This
10+
module is a Rust Wasm app with QuickJS embedded; it has one custom export,
11+
`__invoke`. `__invoke` takes an index (`N`), looks at all the JS exports
12+
in your main module sorted alphabetically, then invokes the `N`th one.
13+
2. Next, we use [Wizer] to partially evaluate the core module. The user passes
14+
in some JS which we evaluate as part of this process. We determine the Wasm
15+
exports by evaluating the JS in this partial evaluation step, reading the
16+
exports from the JS module, sorting them alphabetically, then adding them to
17+
our (Wasm) exports table. We generate code for these functions (or "thunks")
18+
which calls `__invoke` with the appropriate export function ID.
19+
20+
While it works fine for exports, there are some downsides to this approach.
21+
It's complicated and slow, and does not work for imports. For imports, we'd
22+
need to come up with a way to programmatically extract the imports and their
23+
types from the user's JS code. We can't assume we'll know the Wasm signature.
24+
Exports are easy because all Extism exports have the same signature, but
25+
imports may have any valid Wasm signature. This technique works well for
26+
exports in part because it is purely additive: we add new functions at the end
27+
of the module; on the other hand, imports require re-aligning a lot of index
28+
spaces. This path leads to writing a linker, which is out of scope for us right
29+
now.
30+
31+
## Solution
32+
33+
What we need to start doing is have the programmer define for us their interface explicitly. They'll be able to
34+
define the Exports and Imports in an IDL. We also need to use tooling to link the
35+
core module and the user code module so we don't fall down the trap of havign to build a linker.
36+
37+
Here is how I propose the new path will work. This may not be permanent but I think
38+
it will work for 1.0:
39+
40+
![js pdk pipeline](content/009-js-pdk-pipeline.png)
41+
42+
The programmer will define their interface in a typescript file `interface.d.ts`:
43+
44+
```typescript
45+
// this interface is optional and types are defined by you
46+
// to match your host functions
47+
declare module 'extism:host' {
48+
interface user {
49+
myHostFunction1(p: I32, q: I32): I32;
50+
myHostFunction2(p: I32): I64;
51+
}
52+
}
53+
54+
// main is the plugin module
55+
declare module 'main' {
56+
// all extism exports have type () -> i32
57+
export function greet(): I32;
58+
}
59+
```
60+
61+
Step 1 of the CLI will be to generate 2 shim modules `export-shim.wasm` and `import-shim.wasm` from this interface.
62+
The export shim needs to generate some thunk functions as well as import `__invoke` from the core module (which gets
63+
linked later in the pipeline). I spiked a working [prototype of this here](https://gist.github.com/bhelx/41fba8959fe7738a23cd750983341216).
64+
65+
`export-shim.wasm` will be linked with `core.wasm` using [wasm-merge](https://github.com/WebAssembly/binaryen#wasm-merge)
66+
which is kind of like a linker but a little bit higher level. I may also consider using `wasm-ld`
67+
but this seems to work for me and is flexible.
68+
69+
We then wasm-merge the import shim which gives us a unified interface for indirectly calling import functions.
70+
71+
Out of thhis merge comes the final module, but it's yet to be wizened. This is similar to the process
72+
wer have now. We will run the module and eval the user's plugin js code in the egine. Then we freeze
73+
and dump out to the final wasm after a few passes from wasm-opt.
74+
75+
### Rust to JS Bridge
76+
77+
Suppose we have 2 host functions and one export. The shim module that is generated
78+
should look something like this (TODO this is still not accurate yet):
79+
80+
export-shim.wasm:
81+
82+
```wat
83+
(module
84+
; included by us for invoking the js runtime
85+
(import "coremod" "__invoke" (func (;0;) (type 0)))
86+
87+
; the generated thunk for the export
88+
(func (;1;) (type 0) (param i32) (result i32)
89+
local.get 0
90+
call 0)
91+
(export "myExport" (func 1)))
92+
```
93+
94+
For exports, we can leave the behavior the same. For each export we will generate
95+
a thunk function that calls `__invoke`.
96+
97+
For imports we have a bit of a challenge. The JS code, which is executed in the core
98+
module, needs to be able to invoke these host functions.
99+
100+
The JS code (and underlying rust and c code) doesn't know about these functions, their names or their function
101+
indexes at compile time. That's why the import shim will expose a call-indirect to the host functions.
102+
103+
import-shim.wasm:
104+
105+
```wat
106+
(module
107+
(type $int2int (func (param i32) (result i32)))
108+
109+
(import "coremod" "myHostFunc1" (func $myHostFunc1 (type $int2int)))
110+
(import "coremod" "myHostFunc2" (func $myHostFunc2 (type $int2int)))
111+
112+
(table 2 funcref)
113+
(elem (i32.const 0) $myHostFunc1 $myHostFunc2)
114+
115+
(func $callHostFunc (type $int2int)
116+
local.get 0
117+
(call_indirect (type $int2int) (i32.const 0))
118+
)
119+
(export "__invokeHostFunc" (func $callHostFunc))
120+
)
121+
```
122+
123+
It exposes this indirect call with __invokeHostFunc which can be used by the
124+
core quickjs engine module and invoked from javascript.
125+
126+
## Considerations
127+
128+
### WIT
129+
130+
I considered using WIT for the interface but:
131+
132+
1. Typescript is more natural to JS programmers
133+
2. I believe the binding generators come with all the ABI stuff along with it
134+
135+
We can always add WIT support too and support both.

content/009-js-pdk-pipeline.png

332 KB
Loading

0 commit comments

Comments
 (0)