Skip to content

Commit 948f47f

Browse files
authored
Merge pull request AssemblyScript#1 from nearprotocol/near-bindgen
BSON bindings generation for NEAR
2 parents d7f4874 + f6d0f84 commit 948f47f

File tree

11 files changed

+347
-6
lines changed

11 files changed

+347
-6
lines changed

cli/asc.js

+21
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,27 @@ exports.main = function main(argv, options, callback) {
674674
hasOutput = true;
675675
}
676676

677+
// TODO: Make these generators pluggable?
678+
// Write NEAR bindings
679+
if (args.nearFile != null) {
680+
let nearBindings;
681+
if (args.nearFile.length) {
682+
stats.emitCount++;
683+
stats.emitTime += measure(() => {
684+
nearBindings = assemblyscript.buildNEAR(program);
685+
});
686+
writeFile(args.nearFile, nearBindings, baseDir);
687+
} else if (!hasStdout) {
688+
stats.emitCount++;
689+
stats.emitTime += measure(() => {
690+
nearBindings = assemblyscript.buildNEAR(program);
691+
});
692+
writeStdout(nearBindings);
693+
hasStdout = true;
694+
}
695+
hasOutput = true;
696+
}
697+
677698
// Write text (must be last)
678699
if (args.textFile != null || !hasOutput) {
679700
let wat;

cli/asc.json

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@
7474
"type": "s",
7575
"alias": "d"
7676
},
77+
"nearFile": {
78+
"description": "Specifies the NEAR Bindings output file (.near.ts).",
79+
"type": "s"
80+
},
7781
"sourceMap": {
7882
"description": [
7983
"Enables source map generation. Optionally takes the URL",

dist/asc.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/asc.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/assemblyscript.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/assemblyscript.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/definitions.ts

+203
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,209 @@ abstract class ExportsWalker {
127127
abstract visitNamespace(element: Element): void;
128128
}
129129

130+
// TODO: Extract this into separate module, preferrable pluggable
131+
export class NEARBindingsBuilder extends ExportsWalker {
132+
private sb: string[] = [];
133+
134+
private typeMapping: { [key: string]: string } = {
135+
"i32": "Integer",
136+
"String": "String",
137+
"Uint8Array": "Uint8Array",
138+
"bool": "Boolean"
139+
};
140+
141+
private nonNullableTypes = ["i32", "bool"];
142+
143+
static build(program: Program): string {
144+
return new NEARBindingsBuilder(program).build();
145+
}
146+
147+
visitGlobal(element: Global): void {
148+
// Do nothing
149+
}
150+
151+
visitEnum(element: Enum): void {
152+
// Do nothing
153+
}
154+
155+
visitFunction(element: Function): void {
156+
console.log("visitFunction: " + element.simpleName);
157+
let signature = element.signature;
158+
159+
this.sb.push(`export class __near_ArgsParser_${element.simpleName} {
160+
`);
161+
let fields = [];
162+
if (signature.parameterNames) {
163+
for (let i = 0; i < signature.parameterNames.length; i++) {
164+
let paramName = signature.parameterNames[i];
165+
let paramType = signature.parameterTypes[i];
166+
fields.push({
167+
simpleName: paramName,
168+
type: paramType
169+
});
170+
}
171+
}
172+
fields.forEach((field) => {
173+
this.sb.push(`__near_param_${field.simpleName}: ${field.type};`);
174+
});
175+
this.generateBSONHandlerMethods("this.__near_param_", fields);
176+
this.sb.push(`}`); // __near_ArgsParser
177+
178+
let returnType = signature.returnType.toString();
179+
this.sb.push(`export function near_func_${element.simpleName}(): void {
180+
let bson = new Uint8Array(input_read_len());
181+
input_read_into(bson.buffer.data);
182+
let handler = new __near_ArgsParser_${element.simpleName}();
183+
let decoder = new BSONDecoder<__near_ArgsParser_${element.simpleName}>(handler);
184+
decoder.deserialize(bson);`);
185+
if (returnType != "void") {
186+
this.sb.push(`let result = ${element.simpleName}(`);
187+
} else {
188+
this.sb.push(`${element.simpleName}(`)
189+
}
190+
if (signature.parameterNames) {
191+
let i = 0;
192+
for (let paramName of signature.parameterNames) {
193+
this.sb.push(`handler.__near_param_${paramName}`);
194+
if (i < signature.parameterNames.length) {
195+
this.sb.push(",")
196+
}
197+
i++;
198+
}
199+
}
200+
this.sb.push(");");
201+
202+
if (returnType != "void") {
203+
this.sb.push(`
204+
let encoder = new BSONEncoder();`);
205+
this.generateFieldEncoder(returnType, "result", "result");
206+
this.sb.push(`
207+
return_value(near.bufferWithSize(encoder.serialize()).buffer.data);
208+
`);
209+
}
210+
211+
this.sb.push(`}`);
212+
}
213+
214+
private generateBSONHandlerMethods(valuePrefix: string, fields: any[]) : void {
215+
for (let fieldType in this.typeMapping) {
216+
let setterType = this.typeMapping[fieldType];
217+
this.sb.push(`set${setterType}(name: string, value: ${fieldType}): void {`);
218+
fields.forEach((field) => {
219+
if (field.type.toString() == fieldType) {
220+
this.sb.push(`if (name == "${field.simpleName}") { ${valuePrefix}${field.simpleName} = value; return; }`);
221+
}
222+
});
223+
this.sb.push("}");
224+
}
225+
this.sb.push("setNull(name: string): void {");
226+
fields.forEach((field) => {
227+
this.sb.push(`if (name == "${field.simpleName}") {
228+
${valuePrefix}${field.simpleName} = <${field.type.toString()}>null;
229+
}`);
230+
});
231+
this.sb.push("}\n"); // setNull
232+
233+
// TODO: Suport nested objects/arrays
234+
// TODO: This needs some way to get current index in buffer (extract parser state into separte class?),
235+
// TODO: so that we can call method to parse object recursively
236+
// TODO: popObject() should also return false to exit nested parser?
237+
this.sb.push(`
238+
pushObject(name: string): bool { return false; }
239+
popObject(): void {}
240+
pushArray(name: string): bool { return false; }
241+
popArray(): void {}
242+
`);
243+
}
244+
245+
visitClass(element: Class): void {
246+
let className = element.simpleName;
247+
console.log("visitClass: " + className);
248+
this.sb.push(`export function __near_encode_${className}(
249+
value: ${className},
250+
encoder: BSONEncoder): void {`);
251+
this.getFields(element).forEach((field) => {
252+
let fieldType = field.type.toString();
253+
let fieldName = field.simpleName;
254+
let sourceExpr = `value.${fieldName}`;
255+
this.generateFieldEncoder(fieldType, fieldName, sourceExpr);
256+
});
257+
this.sb.push("}"); // __near_encode
258+
259+
this.sb.push(`export class __near_BSONHandler_${className} {
260+
value: ${className} = new ${className}();`);
261+
this.generateBSONHandlerMethods("this.value.", this.getFields(element));
262+
this.sb.push("}\n"); // class __near_BSONHandler_
263+
264+
this.sb.push(`export function __near_decode_${className}(
265+
buffer: Uint8Array, offset: i32): ${className} {
266+
let handler = new __near_BSONHandler_${className}();
267+
let decoder = new BSONDecoder<__near_BSONHandler_${className}>(handler);
268+
decoder.deserialize(buffer, offset);
269+
return handler.value;
270+
}\n`);
271+
}
272+
273+
private generateFieldEncoder(fieldType: any, fieldName: any, sourceExpr: string) {
274+
let setterType = this.typeMapping[fieldType];
275+
if (!setterType) {
276+
// Object
277+
this.sb.push(`if (${sourceExpr} != null) {
278+
__near_encode_${fieldType}(${sourceExpr}, encoder);
279+
} else {
280+
encoder.setNull("${fieldName}");
281+
}`);
282+
}
283+
else {
284+
// Basic types
285+
if (this.nonNullableTypes.indexOf(fieldType) != -1) {
286+
this.sb.push(`encoder.set${setterType}("${fieldName}", ${sourceExpr});`);
287+
}
288+
else {
289+
this.sb.push(`if (${sourceExpr} != null) {
290+
encoder.set${setterType}("${fieldName}", ${sourceExpr});
291+
} else {
292+
encoder.setNull("${fieldName}");
293+
}`);
294+
}
295+
}
296+
}
297+
298+
private getFields(element: Class): any[] {
299+
var members = element.members;
300+
var results = [];
301+
if (members) {
302+
for (let member of members.values()) {
303+
if (!(member instanceof Field)) {
304+
continue;
305+
}
306+
results.push(member);
307+
}
308+
}
309+
return results;
310+
}
311+
312+
visitInterface(element: Interface): void {
313+
// Do nothing
314+
}
315+
316+
visitField(element: Field): void {
317+
throw new Error("Shouldn't be called");
318+
}
319+
320+
visitNamespace(element: Element): void {
321+
// Do nothing
322+
}
323+
324+
build(): string {
325+
let mainSource = this.program.sources
326+
.filter(s => s.normalizedPath.indexOf("~lib") != 0)[0];
327+
this.sb.push(mainSource.text);
328+
this.walk();
329+
return this.sb.join("\n");
330+
}
331+
}
332+
130333
/** A WebIDL definitions builder. */
131334
export class IDLBuilder extends ExportsWalker {
132335

src/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616

1717
import {
1818
IDLBuilder,
19-
TSDBuilder
19+
TSDBuilder,
20+
NEARBindingsBuilder
2021
} from "./definitions";
2122

2223
import {
@@ -167,6 +168,11 @@ export function buildTSD(program: Program): string {
167168
return TSDBuilder.build(program);
168169
}
169170

171+
// TODO: Make pluggable tree walkers instead of hardcoding various formats here
172+
export function buildNEAR(program: Program): string {
173+
return NEARBindingsBuilder.build(program);
174+
}
175+
170176
/** Prefix indicating a library file. */
171177
export { LIBRARY_PREFIX } from "./common";
172178

tests/near-bindgen/main.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import "allocator/arena";
2+
// TODO: Why cannot import from index?
3+
// import { BSONEncoder, BSONDecoder } from "./bson";
4+
import { BSONEncoder } from "./bson/encoder";
5+
import { BSONDecoder } from "./bson/decoder";
6+
7+
@external("env", "log")
8+
declare function log(str: string): void;
9+
10+
// Runtime functions
11+
@external("env", "return_value")
12+
declare function return_value(value_ptr: u32): void;
13+
@external("env", "input_read_len")
14+
declare function input_read_len(): u32;
15+
@external("env", "input_read_into")
16+
declare function input_read_into(ptr: usize): void;
17+
18+
type Address = u64;
19+
20+
export function _init(initialOwner: Address): void {
21+
}
22+
23+
export class FooBar {
24+
foo: i32 = 0;
25+
bar: i32 = 1;
26+
flag: bool;
27+
baz: string = "123";
28+
foobar: Uint8Array;
29+
}
30+
31+
export class ContainerClass {
32+
foobar: FooBar
33+
}
34+
35+
export function add(x: i32, y: i32): i32 {
36+
return x + y;
37+
}
38+
39+
export function getFoobar(container: ContainerClass): FooBar {
40+
return container.foobar;
41+
}

tests/near-bindgen/test.sh

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
BASEDIR=../../
3+
$BASEDIR/bin/asc main.ts --outFile main.wat --nearFile main.near.ts
4+
cp main.near.ts combined.ts
5+
prettier --write combined.ts
6+
$BASEDIR/bin/asc combined.ts -o combined.wat
7+
$BASEDIR/bin/asc test.ts -o test.wasm
8+

tests/near-bindgen/test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
import * as main from "./combined";
3+
4+
import { BSONEncoder } from "./bson/encoder";
5+
6+
@external("env", "log")
7+
declare function log(str: string): void;
8+
9+
export function runTest(): void {
10+
let original = new main.FooBar();
11+
original.foo = 321;
12+
original.bar = 123;
13+
original.flag = true;
14+
original.baz = "foo";
15+
let encoder: BSONEncoder = new BSONEncoder();
16+
main.__near_encode_FooBar(original, encoder);
17+
let encoded = encoder.serialize();
18+
let decoded = main.__near_decode_FooBar(encoded, 0);
19+
20+
assert(original.foo == decoded.foo);
21+
assert(original.bar == decoded.bar);
22+
23+
let argsEncoder: BSONEncoder = new BSONEncoder();
24+
argsEncoder.setInteger("x", 1);
25+
argsEncoder.setInteger("y", 2);
26+
27+
let addBsonStr = bin2hex(argsEncoder.serialize());
28+
let expectedResultEncoder: BSONEncoder = new BSONEncoder();
29+
expectedResultEncoder.setInteger("result", 3);
30+
31+
/*
32+
let bsonResult = main.near_func_add(hex2bin(addBsonStr));
33+
34+
let bsonResultStr = bin2hex(bsonResult);
35+
let expectedBsonResultStr = bin2hex(expectedResultEncoder.serialize())
36+
assert(bsonResultStr == expectedBsonResultStr, bsonResultStr + "\n" + expectedBsonResultStr);
37+
*/
38+
}
39+
40+
function hex2bin(hex: string): Uint8Array {
41+
let bin = new Uint8Array(hex.length >>> 1);
42+
for (let i = 0, len = hex.length >>> 1; i < len; i++) {
43+
bin[i] = u32(parseInt(hex.substr(i << 1, 2), 16));
44+
}
45+
return bin;
46+
}
47+
48+
function bin2hex(bin: Uint8Array, uppercase: boolean = false): string {
49+
let hex = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
50+
let str = "";
51+
for (let i = 0, len = bin.length; i < len; i++) {
52+
str += hex.charAt((bin[i] >>> 4) & 0x0f) + hex.charAt(bin[i] & 0x0f);
53+
}
54+
return str;
55+
}
56+
57+
58+
runTest();

0 commit comments

Comments
 (0)