Skip to content

Commit

Permalink
feat(wasm-api): add preliminary string handling support
Browse files Browse the repository at this point in the history
- update/rename IWasmMemoryAccess (add string getter/setter)
- update StructField.type (add `string`)
- add CodeGenOpts.stringType option
- update codegen fns
- update TS & Zig codegen impls
  • Loading branch information
postspectacular committed Aug 16, 2022
1 parent d32026c commit 3da4efe
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 73 deletions.
47 changes: 41 additions & 6 deletions packages/wasm-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface WasmExports {
_wasm_free(addr: number, numBytes: number): void;
}

export interface WasmMemViews {
export interface IWasmMemoryAccess {
i8: Int8Array;
u8: Uint8Array;
i16: Int16Array;
Expand All @@ -82,6 +82,38 @@ export interface WasmMemViews {
u64: BigUint64Array;
f32: Float32Array;
f64: Float64Array;

/**
* Reads UTF-8 encoded string from given address and optional byte length.
* The default length is 0, which will be interpreted as a zero-terminated
* string. Returns string.
*
* @param addr
* @param len
*/
getString(addr: number, len?: number): string;

/**
* Encodes given string as UTF-8 and writes it to WASM memory starting at
* `addr`. By default the string will be zero-terminated and only `maxBytes`
* will be written. Returns the number of bytes written.
*
* @remarks
* An error will be thrown if the encoded string doesn't fully fit into the
* designated memory region (also note that there might need to be space for
* the additional sentinel/termination byte).
*
* @param str
* @param addr
* @param maxBytes
* @param terminate
*/
setString(
str: string,
addr: number,
maxBytes: number,
terminate?: boolean
): number;
}

/**
Expand Down Expand Up @@ -135,7 +167,7 @@ export interface WasmType<T> {
instance: Fn<number, T>;
}

export type WasmTypeConstructor<T> = Fn<WasmMemViews, WasmType<T>>;
export type WasmTypeConstructor<T> = Fn<IWasmMemoryAccess, WasmType<T>>;

export type WasmInt = "i8" | "i16" | "i32" | "i64";
export type WasmUint = "u8" | "u16" | "u32" | "u64";
Expand Down Expand Up @@ -225,13 +257,16 @@ export interface StructField extends TypeInfo {
*/
tag?: "scalar" | "array" | "ptr" | "slice" | "vec";
/**
* Field base type. If not a {@link WasmPrim} or `opaque`, the value is
* interpreted as another type name in the {@link TypeColl}.
* Field base type. If not a {@link WasmPrim}, `string` or `opaque`, the
* value is interpreted as another type name in the {@link TypeColl}.
*
* @remarks
* Please see {@link CodeGenOpts.stringType} and consult package readme for
* further details re: string handling.
*
* TODO `opaque` currently unsupported.
* TODO add string support (see {@link StructField.sentinel})
*/
type: WasmPrim | "opaque" | string;
type: WasmPrim | "string" | "opaque" | string;
/**
* TODO currently unsupported & ignored!
*/
Expand Down
10 changes: 7 additions & 3 deletions packages/wasm-api/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
CoreAPI,
IWasmAPI,
WasmExports,
WasmMemViews,
IWasmMemoryAccess,
} from "./api.js";

const B32 = BigInt(32);
Expand All @@ -33,7 +33,7 @@ export const OutOfMemoryError = defError(() => "Out of memory");
* support it. No polyfill is provided.
*/
export class WasmBridge<T extends WasmExports = WasmExports>
implements WasmMemViews
implements IWasmMemoryAccess
{
i8!: Int8Array;
u8!: Uint8Array;
Expand Down Expand Up @@ -491,7 +491,11 @@ export class WasmBridge<T extends WasmExports = WasmExports>
this.u8.subarray(addr, addr + maxBytes)
).written!;
if (len == null || len >= maxBytes + (terminate ? 0 : 1)) {
illegalArgs(`error writing string to 0x${U32(addr)}`);
illegalArgs(
`error writing string to 0x${U32(
addr
)} (max. ${maxBytes} bytes, got at least ${str.length})`
);
}
if (terminate) {
this.u8[addr + len!] = 0;
Expand Down
4 changes: 2 additions & 2 deletions packages/wasm-api/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { resolve } from "path";
import type { Struct, TopLevelType, TypeColl } from "./api.js";
import { CodeGenOpts, generateTypes } from "./codegen.js";
import { TSOpts, TYPESCRIPT } from "./codegen/typescript.js";
import { isPrim } from "./codegen/utils.js";
import { isWasmPrim, isWasmString } from "./codegen/utils.js";
import { ZIG, ZigOpts } from "./codegen/zig.js";

const GENERATORS = <const>{ ts: TYPESCRIPT, zig: ZIG };
Expand Down Expand Up @@ -121,7 +121,7 @@ const validateTypeRefs = (coll: TypeColl) => {
for (let spec of Object.values(coll)) {
if (spec.type !== "struct") continue;
for (let f of (<Struct>spec).fields) {
if (!(isPrim(f.type) || coll[f.type])) {
if (!(isWasmPrim(f.type) || isWasmString(f.type) || coll[f.type])) {
invalidSpec(
(<any>spec).__path,
`structfield ${spec.name}.${f.name} of unknown type: ${f.type}`
Expand Down
51 changes: 34 additions & 17 deletions packages/wasm-api/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TypeColl,
USIZE_SIZE,
} from "./api.js";
import { isNumeric } from "./codegen/utils.js";
import { isNumeric, isWasmString } from "./codegen/utils.js";

/**
* Global/shared code generator options.
Expand All @@ -31,13 +31,25 @@ export interface CodeGenOpts {
* codegen's own epilogue, if any)
*/
post: string;
/**
* Identifier how strings are stored on WASM side, e.g. in Zig string
* literals are slices (8 bytes), in C just plain pointers (4 bytes).
*
* @defaultValue "slice"
*/
stringType: "slice" | "ptr";
}

const sizeOf = defmulti<TopLevelType | StructField, TypeColl, number>(
const sizeOf = defmulti<
TopLevelType | StructField,
TypeColl,
CodeGenOpts,
number
>(
(x) => x.type,
{},
{
[DEFAULT]: (field: StructField, types: TypeColl) => {
[DEFAULT]: (field: StructField, types: TypeColl, opts: CodeGenOpts) => {
if (field.__size) return field.__size;
let size = 0;
if (field.tag === "ptr") {
Expand All @@ -47,7 +59,9 @@ const sizeOf = defmulti<TopLevelType | StructField, TypeColl, number>(
} else {
size = isNumeric(field.type)
? SIZEOF[<Type>field.type]
: sizeOf(types[field.type], types);
: isWasmString(field.type)
? USIZE_SIZE * (opts.stringType === "slice" ? 2 : 1)
: sizeOf(types[field.type], types, opts);
if (field.tag == "array" || field.tag === "vec") {
size *= field.len!;
}
Expand All @@ -60,14 +74,14 @@ const sizeOf = defmulti<TopLevelType | StructField, TypeColl, number>(
return (type.__size = SIZEOF[(<Enum>type).tag]);
},

struct: (type, types) => {
struct: (type, types, opts) => {
if (type.__size) return type.__size;
const struct = <Struct>type;
let size = 0;
for (let f of struct.fields) {
size = align(size, <Pow2>f.__align!);
f.__offset = size;
size += sizeOf(f, types);
size += sizeOf(f, types, opts);
}
return (type.__size = align(size, <Pow2>type.__align!));
},
Expand All @@ -82,6 +96,8 @@ const alignOf = defmulti<TopLevelType | StructField, TypeColl, number>(
if (field.__align) return field.__align;
let align = isNumeric(field.type)
? SIZEOF[<Type>field.type]
: isWasmString(field.type)
? USIZE_SIZE
: alignOf(types[field.type], types);
if (field.tag === "vec") {
align *= ceilPow2(field.len!);
Expand All @@ -105,16 +121,16 @@ const alignOf = defmulti<TopLevelType | StructField, TypeColl, number>(
}
);

const prepareType = defmulti<TopLevelType, TypeColl, void>(
const prepareType = defmulti<TopLevelType, TypeColl, CodeGenOpts, void>(
(x) => x.type,
{},
{
[DEFAULT]: (x: TopLevelType, types: TypeColl) => {
[DEFAULT]: (x: TopLevelType, types: TypeColl, opts: CodeGenOpts) => {
if (x.__align && x.__size) return;
alignOf(x, types);
sizeOf(x, types);
sizeOf(x, types, opts);
},
struct: (x, types) => {
struct: (x, types, opts) => {
if (x.__align && x.__size) return;
const struct = <Struct>x;
alignOf(struct, types);
Expand All @@ -125,10 +141,10 @@ const prepareType = defmulti<TopLevelType, TypeColl, void>(
}
for (let f of struct.fields) {
if (types[f.type]) {
prepareType(types[f.type], types);
prepareType(types[f.type], types, opts);
}
}
sizeOf(struct, types);
sizeOf(struct, types, opts);
},
}
);
Expand All @@ -145,9 +161,9 @@ const prepareType = defmulti<TopLevelType, TypeColl, void>(
*
* @internal
*/
export const prepareTypes = (types: TypeColl) => {
export const prepareTypes = (types: TypeColl, opts: CodeGenOpts) => {
for (let id in types) {
prepareType(types[id], types);
prepareType(types[id], types, opts);
}
};

Expand All @@ -171,7 +187,8 @@ export const generateTypes = (
codegen: ICodeGen,
opts: Partial<CodeGenOpts> = {}
) => {
prepareTypes(types);
const $opts = <CodeGenOpts>{ stringType: "slice", ...opts };
prepareTypes(types, $opts);
const res: string[] = [];
codegen.doc(
`Generated by ${PKG_NAME} at ${new Date().toISOString()} - DO NOT EDIT!`,
Expand All @@ -181,13 +198,13 @@ export const generateTypes = (
);
res.push("");
codegen.pre && res.push(codegen.pre, "");
opts.pre && res.push(opts.pre, "");
$opts.pre && res.push($opts.pre, "");
for (let id in types) {
const type = types[id];
type.doc && codegen.doc(type.doc, "", res);
codegen[type.type](<any>type, types, res);
}
opts.post && res.push("", opts.post);
$opts.post && res.push("", $opts.post);
codegen.post && res.push("", codegen.post);
return res.join("\n");
};
Loading

0 comments on commit 3da4efe

Please sign in to comment.