Skip to content

Commit 3f369f4

Browse files
committed
feat: implement jsni
1 parent 567817c commit 3f369f4

6 files changed

Lines changed: 415 additions & 135 deletions

File tree

grower-js/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Grower
2+
Grower: **G**eneral **R**untime **O**n **WE**b browser**R**s **[Now Development]**
3+
4+
A Library for development language runtimes that run on Web Browsers.
5+
6+
## Features
7+
8+
### Grower
9+
10+
```ts
11+
12+
```

grower-js/src/grower.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {GrowerRsImports} from "./types.ts";
2+
import JavaScriptNativeInterface from "./jsni.ts";
3+
import {initSys} from "./sys.ts";
4+
5+
type Options = {
6+
stdOutHandler: (text: string) => void;
7+
stdErrHandler: (text: string) => void;
8+
}
9+
10+
/**
11+
* Grower is the main class for interacting with the Grower Rust WebAssembly module.
12+
*/
13+
export default class Grower {
14+
private readonly imports: GrowerRsImports;
15+
private readonly memory: WebAssembly.Memory;
16+
private readonly options: Options;
17+
18+
private initialized = false;
19+
20+
/**
21+
* JavaScript Native Interface for Grower.
22+
* @see {@link JavaScriptNativeInterface}
23+
*/
24+
readonly jsni: JavaScriptNativeInterface;
25+
26+
/**
27+
* Creates a new instance of Grower.
28+
* @param imports - The functions exported from grower-rs.
29+
* @param memory - The WebAssembly memory instance.
30+
* @param options - Optional configuration for Grower.
31+
*/
32+
constructor(imports: GrowerRsImports, memory: WebAssembly.Memory, options: Partial<Options>) {
33+
this.imports = imports;
34+
this.memory = memory;
35+
this.options = this.normalizeOptions(options);
36+
37+
this.jsni = new JavaScriptNativeInterface(this.imports, this.memory);
38+
}
39+
40+
/**
41+
* Initializes the Grower instance.
42+
* @throws {Error} If Grower is already initialized.
43+
*/
44+
async init() {
45+
if (this.initialized) {
46+
throw new Error("Grower is already initialized.");
47+
}
48+
49+
await initSys(this.options.stdOutHandler, this.options.stdErrHandler);
50+
this.jsni.init();
51+
52+
this.initialized = true;
53+
}
54+
55+
private normalizeOptions(options: Partial<Options>): Options {
56+
return {
57+
stdOutHandler: options.stdOutHandler || ((text: string) => console.log(text)),
58+
stdErrHandler: options.stdErrHandler || ((text: string) => console.error(text)),
59+
};
60+
}
61+
}

grower-js/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from "./sys";
1+
export * from "./grower";
22
export * from "./types";

grower-js/src/jsni.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import {GrowerRsImports, JSNIFunction} from "./types.ts";
2+
3+
const FUNCTIONS = "__grower_jsni_functions";
4+
const ENTRY_POINT = "__grower_jsni_call";
5+
type _Window = Window & typeof global & {
6+
[ENTRY_POINT]: (jsFuncNamePtr: number, argsPtr: number, argsCount: number) => Promise<number>;
7+
[FUNCTIONS]: Record<string, JSNIFunction>;
8+
};
9+
10+
export default class JavaScriptNativeInterface {
11+
12+
private readonly imports: GrowerRsImports;
13+
private readonly memory: WebAssembly.Memory;
14+
15+
constructor(imports: GrowerRsImports, memory: WebAssembly.Memory) {
16+
this.imports = imports;
17+
this.memory = memory;
18+
}
19+
20+
init() {
21+
if (!(window as _Window)[FUNCTIONS]) {
22+
(window as _Window)[FUNCTIONS] = {};
23+
}
24+
25+
(window as _Window)[ENTRY_POINT] = async (jsFuncNamePtr: number, argsPtr: number, argsCount: number): Promise<number> => {
26+
const session = new JSNIFunctionCallingSession(this.imports, this.memory, jsFuncNamePtr, argsPtr, argsCount);
27+
const value = await session.call();
28+
return session.buildReturnValues(value);
29+
};
30+
}
31+
32+
register(name: string, func: JSNIFunction) {
33+
(window as _Window)[FUNCTIONS][name] = func;
34+
}
35+
36+
}
37+
38+
class JSNIFunctionCallingSession {
39+
private imports: GrowerRsImports;
40+
private view: DataView;
41+
private arr: Uint8Array;
42+
private argsPtr: number;
43+
private argsCount: number;
44+
45+
functionName: string;
46+
args: any[] = [];
47+
48+
constructor(
49+
imports: GrowerRsImports,
50+
view: WebAssembly.Memory,
51+
functionNamePtr: number,
52+
argsPtr: number,
53+
argsCount: number
54+
) {
55+
this.imports = imports;
56+
this.view = new DataView(view.buffer);
57+
this.arr = new Uint8Array(view.buffer);
58+
this.argsPtr = argsPtr;
59+
this.argsCount = argsCount;
60+
61+
this.functionName = this.readString(
62+
this.view.getUint32(functionNamePtr, true),
63+
this.view.getUint32(functionNamePtr + 4, true)
64+
);
65+
this.args = this.parseArgs();
66+
}
67+
68+
async call() {
69+
return (window as _Window)[FUNCTIONS][this.functionName](...this.args);
70+
}
71+
72+
private parseArgs(): any[] {
73+
const args: any[] = [];
74+
for (let i = 0; i < this.argsCount; i++) {
75+
const type = this.view.getUint8(this.argsPtr + i * 16 + 8);
76+
switch (type) {
77+
// i8
78+
case 0:
79+
args.push(this.view.getInt8(this.argsPtr + i * 16));
80+
break;
81+
// i16
82+
case 1:
83+
args.push(this.view.getInt16(this.argsPtr + i * 16, true));
84+
break;
85+
// i32
86+
case 2:
87+
args.push(this.view.getInt32(this.argsPtr + i * 16, true));
88+
break;
89+
// i64
90+
case 3:
91+
args.push(this.view.getBigInt64(this.argsPtr + i * 16, true));
92+
break;
93+
// u8
94+
case 4:
95+
args.push(this.view.getUint8(this.argsPtr + i * 16));
96+
break;
97+
// u16
98+
case 5:
99+
args.push(this.view.getUint16(this.argsPtr + i * 16, true));
100+
break;
101+
// u32
102+
case 6:
103+
args.push(this.view.getUint32(this.argsPtr + i * 16, true));
104+
break;
105+
// u64
106+
case 7:
107+
args.push(this.view.getBigUint64(this.argsPtr + i * 16, true));
108+
break;
109+
// f32
110+
case 8:
111+
args.push(this.view.getFloat32(this.argsPtr + i * 16, true));
112+
break;
113+
// f64
114+
case 9:
115+
args.push(this.view.getFloat64(this.argsPtr + i * 16, true));
116+
break;
117+
// String
118+
case 10:
119+
args.push(
120+
this.readString(
121+
this.view.getUint32(this.argsPtr + i * 16, true),
122+
this.view.getUint32(this.argsPtr + i * 16 + 4, true)
123+
)
124+
);
125+
break;
126+
// U8Array
127+
case 11:
128+
args.push(
129+
this.readVec(
130+
this.view.getUint32(this.argsPtr + i * 16, true),
131+
this.view.getUint32(this.argsPtr + i * 16 + 4, true)
132+
)
133+
);
134+
break;
135+
// null
136+
case 13:
137+
args.push(null);
138+
break;
139+
}
140+
}
141+
return args;
142+
}
143+
144+
buildReturnValues(values: any[]): number {
145+
const size = values.length; // 16 bytes per value
146+
const fat_ptr = this.imports.alloc_jsni_value(size);
147+
const ptr = Number(fat_ptr & BigInt(0xffffffff));
148+
149+
for (let i = 0; i < values.length; i++) {
150+
switch (typeof values[i]) {
151+
case "number":
152+
if (Number.isInteger(values[i])) {
153+
this.view.setBigInt64(ptr + i * 16, BigInt(values[i]), true);
154+
this.view.setInt32(ptr + i * 16 + 8, 3, true);
155+
} else {
156+
this.view.setFloat64(ptr + i * 16, values[i], true);
157+
this.view.setInt32(ptr + i * 16 + 8, 9, true);
158+
}
159+
break;
160+
case "bigint":
161+
this.view.setBigInt64(ptr + i * 16, values[i], true);
162+
this.view.setInt32(ptr + i * 16 + 8, 3, true);
163+
break;
164+
case "string":
165+
const strBytes = new TextEncoder().encode(values[i]);
166+
const strFatPtr = this.imports.alloc(strBytes.length + 1); // 1 byte for null terminator
167+
const strPtr = Number(strFatPtr & BigInt(0xffffffff));
168+
this.arr.set(strBytes, strPtr);
169+
this.arr[strPtr + strBytes.length] = 0; // null terminator
170+
this.view.setUint32(ptr + i * 16, Number(strFatPtr >> BigInt(32)), true);
171+
this.view.setInt32(ptr + i * 16 + 8, 10, true);
172+
break;
173+
case "object":
174+
if (values[i] instanceof Uint8Array) {
175+
const vecBytes = values[i];
176+
const vecFatPtr = this.imports.alloc(vecBytes.length);
177+
const vecPtr = Number(vecFatPtr & BigInt(0xffffffff));
178+
this.arr.set(vecBytes, vecPtr);
179+
this.view.setUint32(ptr + i * 16, Number(vecFatPtr >> BigInt(32)), true);
180+
this.view.setInt32(ptr + i * 16 + 8, 11, true);
181+
} else if (values[i] === null) {
182+
this.view.setUint32(ptr + i * 16, 0, true);
183+
this.view.setInt32(ptr + i * 16 + 8, 13, true);
184+
} else {
185+
throw new Error(`Unsupported object type: ${typeof values[i]}`);
186+
}
187+
break;
188+
}
189+
}
190+
191+
return Number(fat_ptr >> BigInt(32));
192+
}
193+
194+
private readString(ptr: number, len: number): string {
195+
return new TextDecoder().decode(this.arr.slice(ptr, ptr + len));
196+
}
197+
198+
private readVec(ptr: number, len: number): Uint8Array {
199+
return this.arr.slice(ptr, ptr + len);
200+
}
201+
}

0 commit comments

Comments
 (0)