From c229976058a94a68e12e5f8693a9444b2805494a Mon Sep 17 00:00:00 2001 From: LJ <81748770+elle-j@users.noreply.github.com> Date: Wed, 17 May 2023 11:13:08 +0200 Subject: [PATCH 01/12] Add initial support for a Bindgen opt-in list. --- bindgen/src/bound-model.ts | 119 +++++++++++++++++++++++++++---------- bindgen/src/context.ts | 3 +- bindgen/src/generator.ts | 6 +- bindgen/src/program.ts | 20 +++++-- bindgen/src/spec.ts | 11 ++++ bindgen/src/spec/model.ts | 26 ++++++++ 6 files changed, 146 insertions(+), 39 deletions(-) diff --git a/bindgen/src/bound-model.ts b/bindgen/src/bound-model.ts index 4e334b4c262..87bab0b6546 100644 --- a/bindgen/src/bound-model.ts +++ b/bindgen/src/bound-model.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { strict as assert } from "assert"; -import { Spec, TypeSpec, MethodSpec } from "./spec"; +import { OptInSpec, Spec, TypeSpec, MethodSpec } from "./spec"; abstract class TypeBase { abstract readonly kind: TypeKind; @@ -29,31 +29,31 @@ abstract class TypeBase { // This is mostly because TS doesn't know that Type covers all types derived from TypeBase. is(kind: Kind): this is Type & { kind: Kind } { - return this.kind == kind; + return this.kind === kind; } isOptional(): this is Template & { name: "util::Optional" }; isOptional(type: string): boolean; isOptional(type?: string): boolean { - return this.isTemplate("util::Optional") && (!type || ("name" in this.args[0] && this.args[0].name == type)); + return this.isTemplate("util::Optional") && (!type || ("name" in this.args[0] && this.args[0].name === type)); } isNullable(): this is Template & { name: "Nullable" }; isNullable(type: string): boolean; isNullable(type?: string): boolean { - return this.isTemplate("Nullable") && (!type || ("name" in this.args[0] && this.args[0].name == type)); + return this.isTemplate("Nullable") && (!type || ("name" in this.args[0] && this.args[0].name === type)); } isTemplate(): this is Template; isTemplate(type: Name): this is Template & { name: Name }; isTemplate(type?: string): boolean { - return this.is("Template") && (!type || this.name == type); + return this.is("Template") && (!type || this.name === type); } isPrimitive(): this is Primitive; isPrimitive(type: Name): this is Primitive & { name: Name }; isPrimitive(type?: string): boolean { - return this.is("Primitive") && (!type || this.name == type); + return this.is("Primitive") && (!type || this.name === type); } isVoid(): this is Primitive & { name: "void" } { @@ -172,7 +172,7 @@ export class Func extends TypeBase { asyncTransform(): Func | undefined { if (!this.ret.isVoid()) return undefined; const voidType = this.ret; - if (this.args.length == 0) return undefined; + if (this.args.length === 0) return undefined; const lastArgType = this.args[this.args.length - 1].type.removeConstRef(); if (!lastArgType.isTemplate("AsyncCallback")) return undefined; @@ -196,7 +196,7 @@ export class Func extends TypeBase { `but got ${lastCbArgType}`, ); let res: Type = voidType; - if (cb.args.length == 2) { + if (cb.args.length === 2) { res = cb.args[0].type.removeConstRef(); if (res.isOptional() || res.isNullable()) { res = res.args[0]; @@ -263,6 +263,8 @@ export type MethodCallSig = ({ self }: { self: string }, ...args: string[]) => s export abstract class Method { isConstructor = false; + /** Whether this method is opted in to by the consumer/SDK. */ + isOptedInTo = false; abstract isStatic: boolean; constructor( public on: Class, @@ -334,11 +336,30 @@ export class Class extends NamedType { abstract = false; base?: Class; subclasses: Class[] = []; - methods: Method[] = []; + private _methods: { [uniqueName: string]: Method } = {}; sharedPtrWrapped = false; needsDeref = false; iterable?: Type; + get methods(): Readonly { + return Object.values(this._methods); + } + + addMethod(method: Method) { + assert( + !(method.unique_name in this._methods), + `Duplicate unique method name on class '${this.name}': '${method.unique_name}'`, + ); + this._methods[method.unique_name] = method; + } + + getMethod(uniqueName: string) { + const method = this._methods[uniqueName]; + assert(method, `Method '${uniqueName}' does not exist on class '${this.name}'.`); + + return method; + } + toString() { return `class ${this.name}`; } @@ -356,16 +377,18 @@ export class Class extends NamedType { return cls; } - *decedents(): Iterable { + *descendants(): Iterable { for (const sub of this.subclasses) { assert.notEqual(sub, this, `base class loop detected on ${this.name}`); yield sub; - yield* sub.decedents(); + yield* sub.descendants(); } } } export class Field { + /** Whether this field is opted in to by the consumer/SDK. */ + isOptedInTo = false; constructor( public name: string, public cppName: string, @@ -378,7 +401,23 @@ export class Field { export class Struct extends NamedType { readonly kind = "Struct"; cppName!: string; - fields: Field[] = []; + private _fields: { [name: string]: Field } = {}; + + get fields(): Readonly { + return Object.values(this._fields); + } + + addField(field: Field) { + assert(!(field.name in this._fields), `Duplicate field name on record/struct '${this.name}': '${field.name}'.`); + this._fields[field.name] = field; + } + + getField(name: string) { + const field = this._fields[name]; + assert(field, `Field '${name}' does not exist on record/struct '${this.name}'.`); + + return field; + } toString() { return `struct ${this.name}`; @@ -485,7 +524,7 @@ type MixedInfo = { ctors: Type[]; }; -export function bindModel(spec: Spec): BoundSpec { +export function bindModel(spec: Spec, optInSpec?: OptInSpec): BoundSpec { const templates: Map = new Map(); const rootClasses: Class[] = []; @@ -493,7 +532,7 @@ export function bindModel(spec: Spec): BoundSpec { function addType(name: string, type: T | (new (name: string) => T)) { assert(!(name in out.types)); - if (typeof type == "function") type = new type(name); + if (typeof type === "function") type = new type(name); out.types[name] = type; return type; @@ -548,7 +587,7 @@ export function bindModel(spec: Spec): BoundSpec { ) { for (const [name, overloads] of Object.entries(methods)) { for (const overload of overloads) { - on.methods.push( + on.addMethod( new OutType( on, name, @@ -561,7 +600,7 @@ export function bindModel(spec: Spec): BoundSpec { } } - // Attach names to instences of Type in types + // Attach names to instances of Type in types for (const [name, args] of Object.entries(spec.templates)) { templates.set(name, args); } @@ -606,12 +645,12 @@ export function bindModel(spec: Spec): BoundSpec { for (const [name, { cppName, fields }] of Object.entries(spec.records)) { const struct = out.types[name] as Struct; struct.cppName = cppName ?? name; - struct.fields = Object.entries(fields).map(([name, field]) => { + for (const [name, field] of Object.entries(fields)) { const type = resolveTypes(field.type); // Optional and Nullable fields are never required. const required = field.default === undefined && !(type.isNullable() || type.isOptional()); - return new Field(name, field.cppName ?? name, type, required, field.default); - }); + struct.addField(new Field(name, field.cppName ?? name, type, required, field.default)); + } } for (const [name, type] of Object.entries(spec.keyTypes)) { @@ -642,23 +681,24 @@ export function bindModel(spec: Spec): BoundSpec { // Constructors are exported to js as named static methods. The "real" js constructors // are only used internally for attaching the C++ instance to a JS object. - cls.methods.push( - ...Object.entries(raw.constructors).flatMap(([name, rawSig]) => { - const sig = resolveTypes(rawSig); - // Constructors implicitly return the type of the class. - assert(sig.kind == "Func" && sig.ret.isVoid()); - sig.ret = cls.sharedPtrWrapped ? new Template("std::shared_ptr", [cls]) : cls; - return new Constructor(cls, name, sig); - }), - ); + const constructors = Object.entries(raw.constructors).flatMap(([name, rawSig]) => { + const sig = resolveTypes(rawSig); + // Constructors implicitly return the type of the class. + assert(sig.kind === "Func" && sig.ret.isVoid()); + sig.ret = cls.sharedPtrWrapped ? new Template("std::shared_ptr", [cls]) : cls; + return new Constructor(cls, name, sig); + }); + for (const constructor of constructors) { + cls.addMethod(constructor); + } for (const [name, type] of Object.entries(raw.properties ?? {})) { - cls.methods.push(new Property(cls, name, resolveTypes(type))); + cls.addMethod(new Property(cls, name, resolveTypes(type))); } } for (const cls of rootClasses) { - out.classes.push(cls, ...cls.decedents()); + out.classes.push(cls, ...cls.descendants()); } out.mixedInfo = { @@ -673,5 +713,24 @@ export function bindModel(spec: Spec): BoundSpec { .concat(Object.values(spec.mixedInfo.dataTypes).map(({ type }) => out.types[type])), }; + // Mark methods and fields in the opt-in list as `isOptedInTo` which the consumer/SDK + // then can choose to handle accordingly. + + for (const [optInClassName, optInRaw] of Object.entries(optInSpec?.classes ?? {})) { + const boundClass = out.types[optInClassName]; + assert(boundClass instanceof Class); + for (const optInMethodName of optInRaw.methods) { + boundClass.getMethod(optInMethodName).isOptedInTo = true; + } + } + + for (const [optInStructName, optInRaw] of Object.entries(optInSpec?.records ?? {})) { + const boundStruct = out.types[optInStructName]; + assert(boundStruct instanceof Struct); + for (const optInFieldName of optInRaw.fields) { + boundStruct.getField(optInFieldName).isOptedInTo = true; + } + } + return out; } diff --git a/bindgen/src/context.ts b/bindgen/src/context.ts index 2079580825a..fb054adbdfb 100644 --- a/bindgen/src/context.ts +++ b/bindgen/src/context.ts @@ -18,10 +18,11 @@ import { Formatter } from "./formatter"; import { Outputter } from "./outputter"; -import { Spec } from "./spec"; +import { OptInSpec, Spec } from "./spec"; export type TemplateContext = { spec: Spec; + optInSpec?: OptInSpec; /** * @param path The file path, relative to the output directory. * @param formatter An optional formatter to run after the template has returned. diff --git a/bindgen/src/generator.ts b/bindgen/src/generator.ts index feb04f448f8..912da95624a 100644 --- a/bindgen/src/generator.ts +++ b/bindgen/src/generator.ts @@ -16,12 +16,13 @@ // //////////////////////////////////////////////////////////////////////////// -import { Spec } from "./spec"; +import { OptInSpec, Spec } from "./spec"; import { Template } from "./templates"; import { createOutputDirectory } from "./output-directory"; type GenerateOptions = { spec: Spec; + optInSpec?: OptInSpec; template: Template; outputPath: string; }; @@ -31,12 +32,13 @@ type GenerateOptions = { * * @param options The spec to generate from, the template to apply and the path of the directory to write output */ -export function generate({ spec, template, outputPath }: GenerateOptions): void { +export function generate({ spec, optInSpec, template, outputPath }: GenerateOptions): void { // Apply the template const outputDirectory = createOutputDirectory(outputPath); try { template({ spec, + optInSpec, file: outputDirectory.file.bind(outputDirectory), }); } finally { diff --git a/bindgen/src/program.ts b/bindgen/src/program.ts index 91a99313255..73fa07c116e 100644 --- a/bindgen/src/program.ts +++ b/bindgen/src/program.ts @@ -23,11 +23,12 @@ import path from "path"; import { debug, enableDebugging } from "./debug"; import { generate } from "./generator"; -import { InvalidSpecError, parseSpecs } from "./spec"; +import { OptInSpec, InvalidSpecError, parseOptInList, parseSpecs } from "./spec"; import { Template, importTemplate } from "./templates"; type GenerateOptions = { spec: ReadonlyArray; + optIn?: string; template: Promise