Skip to content

Commit

Permalink
Implement support for callback functions
Browse files Browse the repository at this point in the history
Supersedes and closes #123.

Co-authored-by: Timothy Gu <timothygu99@gmail.com>
  • Loading branch information
2 people authored and domenic committed May 5, 2020
1 parent 2aab61c commit 4a6e558
Show file tree
Hide file tree
Showing 12 changed files with 847 additions and 44 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- Enumeration types
- Union types
- Callback interfaces
- Callback function types, somewhat
- Callback functions
- Nullable types
- `sequence<>` types
- `record<>` types
Expand All @@ -474,6 +474,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- `[LegacyNoInterfaceObject]`
- `[LegacyNullToEmptyString]`
- `[LegacyOverrideBuiltins]`
- `[LegacyTreatNonObjectAsNull]`
- `[LegacyUnenumerableNamedProperties]`
- `[LegacyUnforgeable]`
- `[LegacyWindowAlias]`
Expand Down
206 changes: 206 additions & 0 deletions lib/constructs/callback-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"use strict";

const conversions = require("webidl-conversions");

const utils = require("../utils.js");
const Types = require("../types.js");

class CallbackFunction {
constructor(ctx, idl) {
this.ctx = ctx;
this.idl = idl;
this.name = idl.name;
this.str = null;

this.requires = new utils.RequiresMap(ctx);

this.legacyTreatNonObjectAsNull = Boolean(utils.getExtAttr(idl.extAttrs, "LegacyTreatNonObjectAsNull"));
}

generateConversion() {
const { idl, legacyTreatNonObjectAsNull } = this;
const isAsync = idl.idlType.generic === "Promise";

const assertCallable = legacyTreatNonObjectAsNull ? "" : `
if (typeof value !== "function") {
throw new TypeError(context + " is not a function");
}
`;

let returnIDL = "";
if (idl.idlType.idlType !== "void") {
const conv = Types.generateTypeConversion(this.ctx, "callResult", idl.idlType, [], this.name, "context");
this.requires.merge(conv.requires);
returnIDL = `
${conv.body}
return callResult;
`;
}

// This is a simplification of https://heycam.github.io/webidl/#web-idl-arguments-list-converting that currently
// fits our needs.
let argsToES = "";
let inputArgs = "";
let applyArgs = "[]";

if (idl.arguments.length > 0) {
if (idl.arguments.every(arg => !arg.optional && !arg.variadic)) {
const argNames = idl.arguments.map(arg => arg.name);
inputArgs = argNames.join(", ");
applyArgs = `[${inputArgs}]`;

for (const arg of idl.arguments) {
const argName = arg.name;
if (arg.idlType.union ?
arg.idlType.idlType.some(type => !conversions[type.idlType]) :
!conversions[arg.idlType.idlType]) {
argsToES += `
${argName} = utils.tryWrapperForImpl(${argName});
`;
}
}
} else {
const maxArgs = idl.arguments.some(arg => arg.variadic) ? Infinity : idl.arguments.length;
let minArgs = 0;

for (const arg of idl.arguments) {
if (arg.optional || arg.variadic) {
break;
}

minArgs++;
}

if (maxArgs > 0) {
inputArgs = "...args";
applyArgs = "args";

const maxArgsLoop = Number.isFinite(maxArgs) ?
`Math.min(args.length, ${maxArgs})` :
"args.length";

argsToES += `
for (let i = 0; i < ${maxArgsLoop}; i++) {
args[i] = utils.tryWrapperForImpl(args[i]);
}
`;

if (minArgs > 0) {
argsToES += `
if (args.length < ${minArgs}) {
for (let i = args.length; i < ${minArgs}; i++) {
args[i] = undefined;
}
}
`;
}

if (Number.isFinite(maxArgs)) {
argsToES += `
${minArgs > 0 ? "else" : ""} if (args.length > ${maxArgs}) {
args.length = ${maxArgs};
}
`;
}
}
}
}

this.str += `
exports.convert = (value, { context = "The provided value" } = {}) => {
${assertCallable}
function invokeTheCallbackFunction(${inputArgs}) {
if (new.target !== undefined) {
throw new Error("Internal error: invokeTheCallbackFunction is not a constructor");
}
const thisArg = utils.tryWrapperForImpl(this);
let callResult;
`;

if (isAsync) {
this.str += `
try {
`;
}

if (legacyTreatNonObjectAsNull) {
this.str += `
if (typeof value === "function") {
`;
}

this.str += `
${argsToES}
callResult = Reflect.apply(value, thisArg, ${applyArgs});
`;

if (legacyTreatNonObjectAsNull) {
this.str += "}";
}

this.str += `
${returnIDL}
`;

if (isAsync) {
this.str += `
} catch (err) {
return Promise.reject(err);
}
`;
}

this.str += `
};
`;

// `[TreatNonObjctAsNull]` and `isAsync` don't apply to
// https://heycam.github.io/webidl/#construct-a-callback-function.
this.str += `
invokeTheCallbackFunction.construct = (${inputArgs}) => {
${argsToES}
let callResult = Reflect.construct(value, ${applyArgs});
${returnIDL}
};
`;

// The wrapperSymbol ensures that if the callback function is used as a return value, that it exposes
// the original callback back. I.e. it implements the conversion from IDL to JS value in
// https://heycam.github.io/webidl/#es-callback-function.
//
// The objectReference is used to implement spec text such as that discussed in
// https://github.com/whatwg/dom/issues/842.
this.str += `
invokeTheCallbackFunction[utils.wrapperSymbol] = value;
invokeTheCallbackFunction.objectReference = value;
return invokeTheCallbackFunction;
};
`;
}

generateRequires() {
this.str = `
${this.requires.generate()}
${this.str}
`;
}

generate() {
this.generateConversion();

this.generateRequires();
}

toString() {
this.str = "";
this.generate();
return this.str;
}
}

CallbackFunction.prototype.type = "callback";

module.exports = CallbackFunction;
13 changes: 6 additions & 7 deletions lib/constructs/iterable.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Iterable {

generate() {
const whence = this.interface.defaultWhence;
const requires = new utils.RequiresMap(this.ctx);

if (this.isPair) {
this.generateFunction("keys", "key");
this.generateFunction("values", "value");
Expand All @@ -42,10 +44,9 @@ class Iterable {
throw new TypeError("Failed to execute 'forEach' on '${this.name}': 1 argument required, " +
"but only 0 present.");
}
if (typeof callback !== "function") {
throw new TypeError("Failed to execute 'forEach' on '${this.name}': The callback provided " +
"as parameter 1 is not a function.");
}
callback = ${requires.addRelative("Function")}.convert(callback, {
context: "Failed to execute 'forEach' on '${this.name}': The callback provided as parameter 1"
});
const thisArg = arguments[1];
let pairs = Array.from(this[implSymbol]);
let i = 0;
Expand All @@ -64,9 +65,7 @@ class Iterable {
// @@iterator is added in Interface class.
}

return {
requires: new utils.RequiresMap(this.ctx)
};
return { requires };
}
}

Expand Down
23 changes: 19 additions & 4 deletions lib/context.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"use strict";
const webidl = require("webidl2");
const CallbackFunction = require("./constructs/callback-function.js");
const Typedef = require("./constructs/typedef");

const builtinTypedefs = webidl.parse(`
const builtinTypes = webidl.parse(`
typedef (Int8Array or Int16Array or Int32Array or
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
Float32Array or Float64Array or DataView) ArrayBufferView;
typedef (ArrayBufferView or ArrayBuffer) BufferSource;
typedef unsigned long long DOMTimeStamp;
callback Function = any (any... arguments);
callback VoidFunction = void ();
`);

function defaultProcessor(code) {
Expand All @@ -20,7 +24,7 @@ class Context {
processCEReactions = defaultProcessor,
processHTMLConstructor = defaultProcessor,
processReflect = null,
options
options = { suppressErrors: false }
} = {}) {
this.implSuffix = implSuffix;
this.processCEReactions = processCEReactions;
Expand All @@ -36,11 +40,19 @@ class Context {
this.interfaces = new Map();
this.interfaceMixins = new Map();
this.callbackInterfaces = new Map();
this.callbackFunctions = new Map();
this.dictionaries = new Map();
this.enumerations = new Map();

for (const typedef of builtinTypedefs) {
this.typedefs.set(typedef.name, new Typedef(this, typedef));
for (const idl of builtinTypes) {
switch (idl.type) {
case "typedef":
this.typedefs.set(idl.name, new Typedef(this, idl));
break;
case "callback":
this.callbackFunctions.set(idl.name, new CallbackFunction(this, idl));
break;
}
}
}

Expand All @@ -54,6 +66,9 @@ class Context {
if (this.callbackInterfaces.has(name)) {
return "callback interface";
}
if (this.callbackFunctions.has(name)) {
return "callback";
}
if (this.dictionaries.has(name)) {
return "dictionary";
}
Expand Down
19 changes: 16 additions & 3 deletions lib/transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const Typedef = require("./constructs/typedef");
const Interface = require("./constructs/interface");
const InterfaceMixin = require("./constructs/interface-mixin");
const CallbackInterface = require("./constructs/callback-interface.js");
const CallbackFunction = require("./constructs/callback-function");
const Dictionary = require("./constructs/dictionary");
const Enumeration = require("./constructs/enumeration");

Expand Down Expand Up @@ -84,7 +85,15 @@ class Transformer {
}));

this.ctx.initialize();
const { interfaces, interfaceMixins, callbackInterfaces, dictionaries, enumerations, typedefs } = this.ctx;
const {
interfaces,
interfaceMixins,
callbackInterfaces,
callbackFunctions,
dictionaries,
enumerations,
typedefs
} = this.ctx;

// first we're gathering all full interfaces and ignore partial ones
for (const file of parsed) {
Expand Down Expand Up @@ -113,6 +122,10 @@ class Transformer {
obj = new CallbackInterface(this.ctx, instruction);
callbackInterfaces.set(obj.name, obj);
break;
case "callback":
obj = new CallbackFunction(this.ctx, instruction);
callbackFunctions.set(obj.name, obj);
break;
case "includes":
break; // handled later
case "dictionary":
Expand Down Expand Up @@ -198,7 +211,7 @@ class Transformer {
const utilsText = await fs.readFile(path.resolve(__dirname, "output/utils.js"));
await fs.writeFile(this.utilPath, utilsText);

const { interfaces, callbackInterfaces, dictionaries, enumerations } = this.ctx;
const { interfaces, callbackInterfaces, callbackFunctions, dictionaries, enumerations } = this.ctx;

let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/");
if (relativeUtils[0] !== ".") {
Expand Down Expand Up @@ -228,7 +241,7 @@ class Transformer {
await fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
}

for (const obj of [...callbackInterfaces.values(), ...dictionaries.values()]) {
for (const obj of [...callbackInterfaces.values(), ...callbackFunctions.values(), ...dictionaries.values()]) {
let source = obj.toString();

source = `
Expand Down
Loading

0 comments on commit 4a6e558

Please sign in to comment.