Skip to content

BSON bindings generation for NEAR #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Dec 20, 2018
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
21 changes: 21 additions & 0 deletions cli/asc.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,27 @@ exports.main = function main(argv, options, callback) {
hasOutput = true;
}

// TODO: Make these generators pluggable?
// Write NEAR bindings
if (args.nearFile != null) {
let nearBindings;
if (args.nearFile.length) {
stats.emitCount++;
stats.emitTime += measure(() => {
nearBindings = assemblyscript.buildNEAR(program);
});
writeFile(args.nearFile, nearBindings, baseDir);
} else if (!hasStdout) {
stats.emitCount++;
stats.emitTime += measure(() => {
nearBindings = assemblyscript.buildNEAR(program);
});
writeStdout(nearBindings);
hasStdout = true;
}
hasOutput = true;
}

// Write text (must be last)
if (args.textFile != null || !hasOutput) {
let wat;
Expand Down
4 changes: 4 additions & 0 deletions cli/asc.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"type": "s",
"alias": "d"
},
"nearFile": {
"description": "Specifies the NEAR Bindings output file (.near.ts).",
"type": "s"
},
"sourceMap": {
"description": [
"Enables source map generation. Optionally takes the URL",
Expand Down
2 changes: 1 addition & 1 deletion dist/asc.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/asc.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/assemblyscript.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/assemblyscript.js.map

Large diffs are not rendered by default.

203 changes: 203 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,209 @@ abstract class ExportsWalker {
abstract visitNamespace(element: Element): void;
}

// TODO: Extract this into separate module, preferrable pluggable
export class NEARBindingsBuilder extends ExportsWalker {
private sb: string[] = [];

private typeMapping: { [key: string]: string } = {
"i32": "Integer",
"String": "String",
"Uint8Array": "Uint8Array",
"bool": "Boolean"
};

private nonNullableTypes = ["i32", "bool"];

static build(program: Program): string {
return new NEARBindingsBuilder(program).build();
}

visitGlobal(element: Global): void {
// Do nothing
}

visitEnum(element: Enum): void {
// Do nothing
}

visitFunction(element: Function): void {
console.log("visitFunction: " + element.simpleName);
let signature = element.signature;

this.sb.push(`export class __near_ArgsParser_${element.simpleName} {
`);
let fields = [];
if (signature.parameterNames) {
for (let i = 0; i < signature.parameterNames.length; i++) {
let paramName = signature.parameterNames[i];
let paramType = signature.parameterTypes[i];
fields.push({
simpleName: paramName,
type: paramType
});
}
}
fields.forEach((field) => {
this.sb.push(`__near_param_${field.simpleName}: ${field.type};`);
});
this.generateBSONHandlerMethods("this.__near_param_", fields);
this.sb.push(`}`); // __near_ArgsParser

let returnType = signature.returnType.toString();
this.sb.push(`export function near_func_${element.simpleName}(): void {
let bson = new Uint8Array(input_read_len());
input_read_into(bson.buffer.data);
let handler = new __near_ArgsParser_${element.simpleName}();
let decoder = new BSONDecoder<__near_ArgsParser_${element.simpleName}>(handler);
decoder.deserialize(bson);`);
if (returnType != "void") {
this.sb.push(`let result = ${element.simpleName}(`);
} else {
this.sb.push(`${element.simpleName}(`)
}
if (signature.parameterNames) {
let i = 0;
for (let paramName of signature.parameterNames) {
this.sb.push(`handler.__near_param_${paramName}`);
if (i < signature.parameterNames.length) {
this.sb.push(",")
}
i++;
}
}
this.sb.push(");");

if (returnType != "void") {
this.sb.push(`
let encoder = new BSONEncoder();`);
this.generateFieldEncoder(returnType, "result", "result");
this.sb.push(`
return_value(near.bufferWithSize(encoder.serialize()).buffer.data);
`);
}

this.sb.push(`}`);
}

private generateBSONHandlerMethods(valuePrefix: string, fields: any[]) : void {
for (let fieldType in this.typeMapping) {
let setterType = this.typeMapping[fieldType];
this.sb.push(`set${setterType}(name: string, value: ${fieldType}): void {`);
fields.forEach((field) => {
if (field.type.toString() == fieldType) {
this.sb.push(`if (name == "${field.simpleName}") { ${valuePrefix}${field.simpleName} = value; return; }`);
}
});
this.sb.push("}");
}
this.sb.push("setNull(name: string): void {");
fields.forEach((field) => {
this.sb.push(`if (name == "${field.simpleName}") {
${valuePrefix}${field.simpleName} = <${field.type.toString()}>null;
}`);
});
this.sb.push("}\n"); // setNull

// TODO: Suport nested objects/arrays
// TODO: This needs some way to get current index in buffer (extract parser state into separte class?),
// TODO: so that we can call method to parse object recursively
// TODO: popObject() should also return false to exit nested parser?
this.sb.push(`
pushObject(name: string): bool { return false; }
popObject(): void {}
pushArray(name: string): bool { return false; }
popArray(): void {}
`);
}

visitClass(element: Class): void {
let className = element.simpleName;
console.log("visitClass: " + className);
this.sb.push(`export function __near_encode_${className}(
value: ${className},
encoder: BSONEncoder): void {`);
this.getFields(element).forEach((field) => {
let fieldType = field.type.toString();
let fieldName = field.simpleName;
let sourceExpr = `value.${fieldName}`;
this.generateFieldEncoder(fieldType, fieldName, sourceExpr);
});
this.sb.push("}"); // __near_encode

this.sb.push(`export class __near_BSONHandler_${className} {
value: ${className} = new ${className}();`);
this.generateBSONHandlerMethods("this.value.", this.getFields(element));
this.sb.push("}\n"); // class __near_BSONHandler_

this.sb.push(`export function __near_decode_${className}(
buffer: Uint8Array, offset: i32): ${className} {
let handler = new __near_BSONHandler_${className}();
let decoder = new BSONDecoder<__near_BSONHandler_${className}>(handler);
decoder.deserialize(buffer, offset);
return handler.value;
}\n`);
}

private generateFieldEncoder(fieldType: any, fieldName: any, sourceExpr: string) {
let setterType = this.typeMapping[fieldType];
if (!setterType) {
// Object
this.sb.push(`if (${sourceExpr} != null) {
__near_encode_${fieldType}(${sourceExpr}, encoder);
} else {
encoder.setNull("${fieldName}");
}`);
}
else {
// Basic types
if (this.nonNullableTypes.indexOf(fieldType) != -1) {
this.sb.push(`encoder.set${setterType}("${fieldName}", ${sourceExpr});`);
}
else {
this.sb.push(`if (${sourceExpr} != null) {
encoder.set${setterType}("${fieldName}", ${sourceExpr});
} else {
encoder.setNull("${fieldName}");
}`);
}
}
}

private getFields(element: Class): any[] {
var members = element.members;
var results = [];
if (members) {
for (let member of members.values()) {
if (!(member instanceof Field)) {
continue;
}
results.push(member);
}
}
return results;
}

visitInterface(element: Interface): void {
// Do nothing
}

visitField(element: Field): void {
throw new Error("Shouldn't be called");
}

visitNamespace(element: Element): void {
// Do nothing
}

build(): string {
let mainSource = this.program.sources
.filter(s => s.normalizedPath.indexOf("~lib") != 0)[0];
this.sb.push(mainSource.text);
this.walk();
return this.sb.join("\n");
}
}

/** A WebIDL definitions builder. */
export class IDLBuilder extends ExportsWalker {

Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {

import {
IDLBuilder,
TSDBuilder
TSDBuilder,
NEARBindingsBuilder
} from "./definitions";

import {
Expand Down Expand Up @@ -167,6 +168,11 @@ export function buildTSD(program: Program): string {
return TSDBuilder.build(program);
}

// TODO: Make pluggable tree walkers instead of hardcoding various formats here
export function buildNEAR(program: Program): string {
return NEARBindingsBuilder.build(program);
}

/** Prefix indicating a library file. */
export { LIBRARY_PREFIX } from "./common";

Expand Down
41 changes: 41 additions & 0 deletions tests/near-bindgen/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "allocator/arena";
// TODO: Why cannot import from index?
// import { BSONEncoder, BSONDecoder } from "./bson";
import { BSONEncoder } from "./bson/encoder";
import { BSONDecoder } from "./bson/decoder";

@external("env", "log")
declare function log(str: string): void;

// Runtime functions
@external("env", "return_value")
declare function return_value(value_ptr: u32): void;
@external("env", "input_read_len")
declare function input_read_len(): u32;
@external("env", "input_read_into")
declare function input_read_into(ptr: usize): void;

type Address = u64;

export function _init(initialOwner: Address): void {
}

export class FooBar {
foo: i32 = 0;
bar: i32 = 1;
flag: bool;
baz: string = "123";
foobar: Uint8Array;
}

export class ContainerClass {
foobar: FooBar
}

export function add(x: i32, y: i32): i32 {
return x + y;
}

export function getFoobar(container: ContainerClass): FooBar {
return container.foobar;
}
8 changes: 8 additions & 0 deletions tests/near-bindgen/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
BASEDIR=../../
$BASEDIR/bin/asc main.ts --outFile main.wat --nearFile main.near.ts
cp main.near.ts combined.ts
prettier --write combined.ts
$BASEDIR/bin/asc combined.ts -o combined.wat
$BASEDIR/bin/asc test.ts -o test.wasm

58 changes: 58 additions & 0 deletions tests/near-bindgen/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

import * as main from "./combined";

import { BSONEncoder } from "./bson/encoder";

@external("env", "log")
declare function log(str: string): void;

export function runTest(): void {
let original = new main.FooBar();
original.foo = 321;
original.bar = 123;
original.flag = true;
original.baz = "foo";
let encoder: BSONEncoder = new BSONEncoder();
main.__near_encode_FooBar(original, encoder);
let encoded = encoder.serialize();
let decoded = main.__near_decode_FooBar(encoded, 0);

assert(original.foo == decoded.foo);
assert(original.bar == decoded.bar);

let argsEncoder: BSONEncoder = new BSONEncoder();
argsEncoder.setInteger("x", 1);
argsEncoder.setInteger("y", 2);

let addBsonStr = bin2hex(argsEncoder.serialize());
let expectedResultEncoder: BSONEncoder = new BSONEncoder();
expectedResultEncoder.setInteger("result", 3);

/*
let bsonResult = main.near_func_add(hex2bin(addBsonStr));

let bsonResultStr = bin2hex(bsonResult);
let expectedBsonResultStr = bin2hex(expectedResultEncoder.serialize())
assert(bsonResultStr == expectedBsonResultStr, bsonResultStr + "\n" + expectedBsonResultStr);
*/
}

function hex2bin(hex: string): Uint8Array {
let bin = new Uint8Array(hex.length >>> 1);
for (let i = 0, len = hex.length >>> 1; i < len; i++) {
bin[i] = u32(parseInt(hex.substr(i << 1, 2), 16));
}
return bin;
}

function bin2hex(bin: Uint8Array, uppercase: boolean = false): string {
let hex = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
let str = "";
for (let i = 0, len = bin.length; i < len; i++) {
str += hex.charAt((bin[i] >>> 4) & 0x0f) + hex.charAt(bin[i] & 0x0f);
}
return str;
}


runTest();