Skip to content
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

Implement support for callback functions #194

Closed
Closed
173 changes: 173 additions & 0 deletions lib/constructs/callback-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"use strict";

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

class CallbackFunction {
/**
* @param {import("../context.js")} ctx
* @param {import("webidl2").CallbackType} idl
*/
constructor(ctx, idl) {
this.ctx = ctx;
this.idl = idl;
this.name = idl.name;
this.str = null;

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

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

const treatNonObjectAsNull = Boolean(utils.getExtAttr(idl.extAttrs, "TreatNonObjectAsNull"));
const assertCallable = treatNonObjectAsNull ? "" : `
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.
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++;
}

let argsToES = "";
if (maxArgs > 0) {
argsToES += `
for (let i = 0; i < ${Number.isFinite(maxArgs) ? `Math.min(args.length, ${maxArgs})` : "args.length"}; 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(${maxArgs > 0 ? "...args" : ""}) {
const thisArg = utils.tryWrapperForImpl(this);
let callResult;
`;

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

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

this.str += `
${argsToES}
callResult = Reflect.apply(value, thisArg, ${maxArgs > 0 ? "args" : "[]"});
`;

if (treatNonObjectAsNull) {
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 = (${maxArgs > 0 ? "...args" : ""}) => {
${argsToES}
let callResult = Reflect.construct(value, ${maxArgs > 0 ? "args" : "[]"});
${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";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CallbackFunction.prototype.type = "callback";
CallbackFunction.prototype.type = "callback function";

Change others as appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


module.exports = CallbackFunction;
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
34 changes: 23 additions & 11 deletions lib/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function mergeExtAttrs(a = [], b = []) {
}

// Types of types that generate an output file.
const resolvedTypes = new Set(["callback interface", "dictionary", "enumeration", "interface"]);
const resolvedTypes = new Set(["callback", "callback interface", "dictionary", "enumeration", "interface"]);

function resolveType(ctx, idlType, stack = []) {
if (resolvedMap.has(idlType)) {
Expand Down Expand Up @@ -113,7 +113,12 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e
} else if (idlType.generic === "FrozenArray") {
// frozen array type
generateFrozenArray();
} else if (conversions[idlType.idlType]) {
} else if (
// TODO: Revert once `Function` and `VoidFunction` are removed from `webidl-conversions`:
idlType.idlType !== "Function" &&
idlType.idlType !== "VoidFunction" &&
conversions[idlType.idlType]
) {
// string or number type compatible with webidl-conversions
generateGeneric(`conversions["${idlType.idlType}"]`);
} else if (resolvedTypes.has(ctx.typeOf(idlType.idlType))) {
Expand Down Expand Up @@ -201,11 +206,14 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e
output.push(`if (${condition}) {}`);
}

if (union.callback || union.object) {
output.push(`if (typeof ${name} === "function") {}`);
}

if (union.sequenceLike || union.dictionary || union.record || union.object || union.callbackInterface) {
if (
union.sequenceLike ||
union.dictionary ||
union.record ||
union.object ||
union.callback ||
union.callbackInterface
) {
ExE-Boss marked this conversation as resolved.
Show resolved Hide resolved
let code = `if (utils.isObject(${name})) {`;

if (union.sequenceLike) {
Expand All @@ -227,6 +235,11 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e
`${errPrefix} + " record"`);
requires.merge(conv.requires);
code += conv.body;
} else if (union.callback) {
const conv = generateTypeConversion(ctx, name, union.callback, [], parentName,
`${errPrefix} + " callback function"`);
requires.merge(conv.requires);
code += conv.body;
} else if (union.callbackInterface) {
const conv = generateTypeConversion(ctx, name, union.callbackInterface, [], parentName,
`${errPrefix} + " callback interface"`);
Expand Down Expand Up @@ -402,7 +415,7 @@ function extractUnionInfo(ctx, idlType, errPrefix) {
numeric: null,
boolean: null,
// Callback function, not interface
callback: false,
callback: null,
ExE-Boss marked this conversation as resolved.
Show resolved Hide resolved
dictionary: null,
callbackInterface: null,
interfaces: new Set(),
Expand Down Expand Up @@ -470,15 +483,14 @@ function extractUnionInfo(ctx, idlType, errPrefix) {
seen.object = true;
} else if (item.idlType === "boolean") {
seen.boolean = item;
} else if (item.idlType === "Function") {
// TODO: add full support for callback functions
} else if (ctx.callbackFunctions.has(item.idlType)) {
if (seen.object) {
error("Callback functions are not distinguishable with object type");
}
if (seen.dictionaryLike) {
error("Callback functions are not distinguishable with dictionary-like types");
}
seen.callback = true;
seen.callback = item.idlType;
} else if (ctx.dictionaries.has(item.idlType)) {
if (seen.object) {
error("Dictionary-like types are not distinguishable with object type");
Expand Down
Loading