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
4 changes: 2 additions & 2 deletions 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 All @@ -496,7 +497,6 @@ Notable missing features include:
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
- `[LegacyFactoryFunction]`
- `[LegacyNamespace]`
- `[LegacyTreatNonObjectAsNull]`
- `[SecureContext]`

## Nonstandard extended attributes
Expand Down
210 changes: 210 additions & 0 deletions lib/constructs/callback-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"use strict";

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

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);

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";
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;
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