Skip to content

Commit

Permalink
Implement support for callback interfaces (#172)
Browse files Browse the repository at this point in the history
Closes #178 by superseding it.

Co-authored-by: Domenic Denicola <[email protected]>
  • Loading branch information
ExE-Boss and domenic authored Apr 3, 2020
1 parent 6b27842 commit 06eb4ae
Show file tree
Hide file tree
Showing 11 changed files with 664 additions and 71 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,20 @@ This function is mostly used internally, and almost never should be called by yo

jsdom does this for `Window`, which is written in custom, non-webidl2js-generated code, but inherits from `EventTarget`, which is generated by webidl2js.

### For callback interfaces

#### `convert(value, { context })`

Performs the Web IDL conversion algorithm for this callback interface, converting _value_ into a function that performs [call a user object's operation](https://heycam.github.io/webidl/#call-a-user-objects-operation) when called, with _thisArg_ being the `this` value of the converted function.

The resulting function has an _objectReference_ property, which is the same object as _value_ and can be used to perform identity checks, as `convert` returns a new function object every time.

If any part of the conversion fails, _context_ can be used to describe the provided value in any resulting error message.

#### `install(globalObject)`

If this callback interface has constants, then this method creates a brand new legacy callback interface object and attaches it to the passed `globalObject`. Otherwise, this method is a no-op.

### For dictionaries

#### `convert(value, { context })`
Expand Down Expand Up @@ -425,6 +439,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- Dictionary types
- Enumeration types
- Union types
- Callback interfaces
- Callback function types, somewhat
- Nullable types
- `sequence<>` types
Expand Down Expand Up @@ -457,7 +472,6 @@ Supported Web IDL extensions defined in HTML:
Notable missing features include:

- Namespaces
- Callback interfaces
- `maplike<>` and `setlike<>`
- `[AllowShared]`
- `[Default]` (for `toJSON()` operations)
Expand Down
237 changes: 237 additions & 0 deletions lib/constructs/callback-interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
"use strict";

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

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

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

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

this.operation = null;
this.constants = new Map();

this._analyzed = false;
this._outputStaticProperties = new Map();
}

_analyzeMembers() {
for (const member of this.idl.members) {
switch (member.type) {
case "operation":
if (this.operation !== null) {
throw new Error(
`Callback interface ${this.name} has more than one operation`
);
}
this.operation = member;
break;
case "const":
this.constants.set(member.name, new Constant(this.ctx, this, member));
break;
default:
throw new Error(
`Illegal IDL member type "${member.type}" in callback interface ${this.name}`
);
}
}

if (this.operation === null) {
throw new Error(`Callback interface ${this.name} has no operation`);
}
}

addAllProperties() {
for (const member of this.constants.values()) {
const data = member.generate();
this.requires.merge(data.requires);
}
}

addStaticProperty(name, body, { configurable = true, enumerable = typeof name === "string", writable = true } = {}) {
const descriptor = { configurable, enumerable, writable };
this._outputStaticProperties.set(name, { body, descriptor });
}

// This is necessary due to usage in the `Constant` and other classes
// It's empty because callback interfaces don't generate platform objects
addProperty() {}

generateConversion() {
const { operation, name } = this;
const opName = operation.name;
const isAsync = operation.idlType.generic === "Promise";

const argNames = operation.arguments.map(arg => arg.name);
if (operation.arguments.some(arg => arg.optional || arg.variadic)) {
throw new Error("Internal error: optional/variadic arguments are not implemented for callback interfaces");
}

this.str += `
exports.convert = function convert(value, { context = "The provided value" } = {}) {
if (!utils.isObject(value)) {
throw new TypeError(\`\${context} is not an object.\`);
}
function callTheUserObjectsOperation(${argNames.join(", ")}) {
let thisArg = this;
let O = value;
let X = O;
`;

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

this.str += `
if (typeof O !== "function") {
X = O[${utils.stringifyPropertyName(opName)}];
if (typeof X !== "function") {
throw new TypeError(\`\${context} does not correctly implement ${name}.\`)
}
thisArg = O;
}
`;

// We don't implement all of https://heycam.github.io/webidl/#web-idl-arguments-list-converting since the callers
// are assumed to always pass the correct number of arguments and we don't support optional/variadic arguments.
// See also: https://github.com/jsdom/webidl2js/issues/71
for (const arg of operation.arguments) {
const argName = arg.name;
if (arg.idlType.union ?
arg.idlType.idlType.some(type => !conversions[type]) :
!conversions[arg.idlType.idlType]) {
this.str += `
${argName} = utils.tryWrapperForImpl(${argName});
`;
}
}

this.str += `
let callResult = Reflect.apply(X, thisArg, [${argNames.join(", ")}]);
`;

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

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

this.str += `
};
`;

// The wrapperSymbol ensures that if the callback interface is used as a return value, e.g. in NodeIterator's filter
// attribute, 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-interface.
//
// The objectReference is used to implement spec text such as that discussed in
// https://github.com/whatwg/dom/issues/842.
this.str += `
callTheUserObjectsOperation[utils.wrapperSymbol] = value;
callTheUserObjectsOperation.objectReference = value;
return callTheUserObjectsOperation;
};
`;
}

generateOffInstanceAfterClass() {
const classProps = new Map();

for (const [name, { body, descriptor }] of this._outputStaticProperties) {
const descriptorModifier = utils.getPropertyDescriptorModifier(
utils.defaultDefinePropertyDescriptor,
descriptor,
"regular",
body
);
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}

if (classProps.size > 0) {
const props = [...classProps].map(([name, body]) => `${name}: ${body}`);
this.str += `
Object.defineProperties(${this.name}, { ${props.join(", ")} });
`;
}
}

generateInstall() {
this.str += `
exports.install = function install(globalObject) {
`;

if (this.constants.size > 0) {
const { name } = this;

this.str += `
const ${name} = () => {
throw new TypeError("Illegal invocation");
};
`;

this.generateOffInstanceAfterClass();

this.str += `
Object.defineProperty(globalObject, ${JSON.stringify(name)}, {
configurable: true,
writable: true,
value: ${name}
});
`;
}

this.str += `
};
`;
}

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

generate() {
this.generateConversion();
this.generateInstall();

this.generateRequires();
}

toString() {
this.str = "";
if (!this._analyzed) {
this._analyzed = true;
this._analyzeMembers();
}
this.addAllProperties();
this.generate();
return this.str;
}
}

module.exports = CallbackInterface;
41 changes: 7 additions & 34 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ function formatArgs(args) {
return args.map(name => name + (keywords.has(name) ? "_" : "")).join(", ");
}

const defaultDefinePropertyDescriptor = {
configurable: false,
enumerable: false,
writable: false
};

const defaultObjectLiteralDescriptor = {
configurable: true,
enumerable: true,
Expand All @@ -39,28 +33,6 @@ const defaultClassMethodDescriptor = {
writable: true
};

// type can be "accessor" or "regular"
function getPropertyDescriptorModifier(currentDesc, targetDesc, type, value = undefined) {
const changes = [];
if (value !== undefined) {
changes.push(`value: ${value}`);
}
if (currentDesc.configurable !== targetDesc.configurable) {
changes.push(`configurable: ${targetDesc.configurable}`);
}
if (currentDesc.enumerable !== targetDesc.enumerable) {
changes.push(`enumerable: ${targetDesc.enumerable}`);
}
if (type !== "accessor" && currentDesc.writable !== targetDesc.writable) {
changes.push(`writable: ${targetDesc.writable}`);
}

if (changes.length === 0) {
return undefined;
}
return `{ ${changes.join(", ")} }`;
}

class Interface {
constructor(ctx, idl, opts) {
this.ctx = ctx;
Expand Down Expand Up @@ -1333,15 +1305,15 @@ class Interface {
continue;
}

const descriptorModifier = getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}

for (const [name, { type, descriptor }] of this._outputStaticMethods) {
const descriptorModifier = getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
Expand All @@ -1354,13 +1326,13 @@ class Interface {
}

const descriptorModifier =
getPropertyDescriptorModifier(defaultDefinePropertyDescriptor, descriptor, "regular", body);
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}

for (const [name, { body, descriptor }] of this._outputStaticProperties) {
const descriptorModifier =
getPropertyDescriptorModifier(defaultDefinePropertyDescriptor, descriptor, "regular", body);
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}

Expand Down Expand Up @@ -1402,7 +1374,7 @@ class Interface {
}
}

const descriptorModifier = getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
Expand All @@ -1417,7 +1389,8 @@ class Interface {
const propName = utils.stringifyPropertyKey(name);
methods.push(`${propName}: ${body}`);

const descriptorModifier = getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, "regular");
const descriptorModifier =
utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, "regular");
if (descriptorModifier === undefined) {
continue;
}
Expand Down
Loading

0 comments on commit 06eb4ae

Please sign in to comment.