Skip to content

Commit 06eb4ae

Browse files
ExE-Bossdomenic
andauthored
Implement support for callback interfaces (#172)
Closes #178 by superseding it. Co-authored-by: Domenic Denicola <[email protected]>
1 parent 6b27842 commit 06eb4ae

File tree

11 files changed

+664
-71
lines changed

11 files changed

+664
-71
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,20 @@ This function is mostly used internally, and almost never should be called by yo
286286

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

289+
### For callback interfaces
290+
291+
#### `convert(value, { context })`
292+
293+
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.
294+
295+
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.
296+
297+
If any part of the conversion fails, _context_ can be used to describe the provided value in any resulting error message.
298+
299+
#### `install(globalObject)`
300+
301+
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.
302+
289303
### For dictionaries
290304

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

459474
- Namespaces
460-
- Callback interfaces
461475
- `maplike<>` and `setlike<>`
462476
- `[AllowShared]`
463477
- `[Default]` (for `toJSON()` operations)
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"use strict";
2+
3+
const conversions = require("webidl-conversions");
4+
5+
const utils = require("../utils.js");
6+
const Types = require("../types.js");
7+
const Constant = require("./constant.js");
8+
9+
class CallbackInterface {
10+
constructor(ctx, idl) {
11+
this.ctx = ctx;
12+
this.idl = idl;
13+
this.name = idl.name;
14+
this.str = null;
15+
16+
this.requires = new utils.RequiresMap(ctx);
17+
18+
this.operation = null;
19+
this.constants = new Map();
20+
21+
this._analyzed = false;
22+
this._outputStaticProperties = new Map();
23+
}
24+
25+
_analyzeMembers() {
26+
for (const member of this.idl.members) {
27+
switch (member.type) {
28+
case "operation":
29+
if (this.operation !== null) {
30+
throw new Error(
31+
`Callback interface ${this.name} has more than one operation`
32+
);
33+
}
34+
this.operation = member;
35+
break;
36+
case "const":
37+
this.constants.set(member.name, new Constant(this.ctx, this, member));
38+
break;
39+
default:
40+
throw new Error(
41+
`Illegal IDL member type "${member.type}" in callback interface ${this.name}`
42+
);
43+
}
44+
}
45+
46+
if (this.operation === null) {
47+
throw new Error(`Callback interface ${this.name} has no operation`);
48+
}
49+
}
50+
51+
addAllProperties() {
52+
for (const member of this.constants.values()) {
53+
const data = member.generate();
54+
this.requires.merge(data.requires);
55+
}
56+
}
57+
58+
addStaticProperty(name, body, { configurable = true, enumerable = typeof name === "string", writable = true } = {}) {
59+
const descriptor = { configurable, enumerable, writable };
60+
this._outputStaticProperties.set(name, { body, descriptor });
61+
}
62+
63+
// This is necessary due to usage in the `Constant` and other classes
64+
// It's empty because callback interfaces don't generate platform objects
65+
addProperty() {}
66+
67+
generateConversion() {
68+
const { operation, name } = this;
69+
const opName = operation.name;
70+
const isAsync = operation.idlType.generic === "Promise";
71+
72+
const argNames = operation.arguments.map(arg => arg.name);
73+
if (operation.arguments.some(arg => arg.optional || arg.variadic)) {
74+
throw new Error("Internal error: optional/variadic arguments are not implemented for callback interfaces");
75+
}
76+
77+
this.str += `
78+
exports.convert = function convert(value, { context = "The provided value" } = {}) {
79+
if (!utils.isObject(value)) {
80+
throw new TypeError(\`\${context} is not an object.\`);
81+
}
82+
83+
function callTheUserObjectsOperation(${argNames.join(", ")}) {
84+
let thisArg = this;
85+
let O = value;
86+
let X = O;
87+
`;
88+
89+
if (isAsync) {
90+
this.str += `
91+
try {
92+
`;
93+
}
94+
95+
this.str += `
96+
if (typeof O !== "function") {
97+
X = O[${utils.stringifyPropertyName(opName)}];
98+
if (typeof X !== "function") {
99+
throw new TypeError(\`\${context} does not correctly implement ${name}.\`)
100+
}
101+
thisArg = O;
102+
}
103+
`;
104+
105+
// We don't implement all of https://heycam.github.io/webidl/#web-idl-arguments-list-converting since the callers
106+
// are assumed to always pass the correct number of arguments and we don't support optional/variadic arguments.
107+
// See also: https://github.com/jsdom/webidl2js/issues/71
108+
for (const arg of operation.arguments) {
109+
const argName = arg.name;
110+
if (arg.idlType.union ?
111+
arg.idlType.idlType.some(type => !conversions[type]) :
112+
!conversions[arg.idlType.idlType]) {
113+
this.str += `
114+
${argName} = utils.tryWrapperForImpl(${argName});
115+
`;
116+
}
117+
}
118+
119+
this.str += `
120+
let callResult = Reflect.apply(X, thisArg, [${argNames.join(", ")}]);
121+
`;
122+
123+
if (operation.idlType.idlType !== "void") {
124+
const conv = Types.generateTypeConversion(this.ctx, "callResult", operation.idlType, [], name, "context");
125+
this.requires.merge(conv.requires);
126+
this.str += `
127+
${conv.body}
128+
return callResult;
129+
`;
130+
}
131+
132+
if (isAsync) {
133+
this.str += `
134+
} catch (err) {
135+
return Promise.reject(err);
136+
}
137+
`;
138+
}
139+
140+
this.str += `
141+
};
142+
`;
143+
144+
// The wrapperSymbol ensures that if the callback interface is used as a return value, e.g. in NodeIterator's filter
145+
// attribute, that it exposes the original callback back. I.e. it implements the conversion from IDL to JS value in
146+
// https://heycam.github.io/webidl/#es-callback-interface.
147+
//
148+
// The objectReference is used to implement spec text such as that discussed in
149+
// https://github.com/whatwg/dom/issues/842.
150+
this.str += `
151+
callTheUserObjectsOperation[utils.wrapperSymbol] = value;
152+
callTheUserObjectsOperation.objectReference = value;
153+
154+
return callTheUserObjectsOperation;
155+
};
156+
`;
157+
}
158+
159+
generateOffInstanceAfterClass() {
160+
const classProps = new Map();
161+
162+
for (const [name, { body, descriptor }] of this._outputStaticProperties) {
163+
const descriptorModifier = utils.getPropertyDescriptorModifier(
164+
utils.defaultDefinePropertyDescriptor,
165+
descriptor,
166+
"regular",
167+
body
168+
);
169+
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
170+
}
171+
172+
if (classProps.size > 0) {
173+
const props = [...classProps].map(([name, body]) => `${name}: ${body}`);
174+
this.str += `
175+
Object.defineProperties(${this.name}, { ${props.join(", ")} });
176+
`;
177+
}
178+
}
179+
180+
generateInstall() {
181+
this.str += `
182+
exports.install = function install(globalObject) {
183+
`;
184+
185+
if (this.constants.size > 0) {
186+
const { name } = this;
187+
188+
this.str += `
189+
const ${name} = () => {
190+
throw new TypeError("Illegal invocation");
191+
};
192+
`;
193+
194+
this.generateOffInstanceAfterClass();
195+
196+
this.str += `
197+
Object.defineProperty(globalObject, ${JSON.stringify(name)}, {
198+
configurable: true,
199+
writable: true,
200+
value: ${name}
201+
});
202+
`;
203+
}
204+
205+
this.str += `
206+
};
207+
`;
208+
}
209+
210+
generateRequires() {
211+
this.str = `
212+
${this.requires.generate()}
213+
214+
${this.str}
215+
`;
216+
}
217+
218+
generate() {
219+
this.generateConversion();
220+
this.generateInstall();
221+
222+
this.generateRequires();
223+
}
224+
225+
toString() {
226+
this.str = "";
227+
if (!this._analyzed) {
228+
this._analyzed = true;
229+
this._analyzeMembers();
230+
}
231+
this.addAllProperties();
232+
this.generate();
233+
return this.str;
234+
}
235+
}
236+
237+
module.exports = CallbackInterface;

lib/constructs/interface.js

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ function formatArgs(args) {
2121
return args.map(name => name + (keywords.has(name) ? "_" : "")).join(", ");
2222
}
2323

24-
const defaultDefinePropertyDescriptor = {
25-
configurable: false,
26-
enumerable: false,
27-
writable: false
28-
};
29-
3024
const defaultObjectLiteralDescriptor = {
3125
configurable: true,
3226
enumerable: true,
@@ -39,28 +33,6 @@ const defaultClassMethodDescriptor = {
3933
writable: true
4034
};
4135

42-
// type can be "accessor" or "regular"
43-
function getPropertyDescriptorModifier(currentDesc, targetDesc, type, value = undefined) {
44-
const changes = [];
45-
if (value !== undefined) {
46-
changes.push(`value: ${value}`);
47-
}
48-
if (currentDesc.configurable !== targetDesc.configurable) {
49-
changes.push(`configurable: ${targetDesc.configurable}`);
50-
}
51-
if (currentDesc.enumerable !== targetDesc.enumerable) {
52-
changes.push(`enumerable: ${targetDesc.enumerable}`);
53-
}
54-
if (type !== "accessor" && currentDesc.writable !== targetDesc.writable) {
55-
changes.push(`writable: ${targetDesc.writable}`);
56-
}
57-
58-
if (changes.length === 0) {
59-
return undefined;
60-
}
61-
return `{ ${changes.join(", ")} }`;
62-
}
63-
6436
class Interface {
6537
constructor(ctx, idl, opts) {
6638
this.ctx = ctx;
@@ -1333,15 +1305,15 @@ class Interface {
13331305
continue;
13341306
}
13351307

1336-
const descriptorModifier = getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
1308+
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
13371309
if (descriptorModifier === undefined) {
13381310
continue;
13391311
}
13401312
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
13411313
}
13421314

13431315
for (const [name, { type, descriptor }] of this._outputStaticMethods) {
1344-
const descriptorModifier = getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
1316+
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
13451317
if (descriptorModifier === undefined) {
13461318
continue;
13471319
}
@@ -1354,13 +1326,13 @@ class Interface {
13541326
}
13551327

13561328
const descriptorModifier =
1357-
getPropertyDescriptorModifier(defaultDefinePropertyDescriptor, descriptor, "regular", body);
1329+
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
13581330
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
13591331
}
13601332

13611333
for (const [name, { body, descriptor }] of this._outputStaticProperties) {
13621334
const descriptorModifier =
1363-
getPropertyDescriptorModifier(defaultDefinePropertyDescriptor, descriptor, "regular", body);
1335+
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
13641336
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
13651337
}
13661338

@@ -1402,7 +1374,7 @@ class Interface {
14021374
}
14031375
}
14041376

1405-
const descriptorModifier = getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
1377+
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
14061378
if (descriptorModifier === undefined) {
14071379
continue;
14081380
}
@@ -1417,7 +1389,8 @@ class Interface {
14171389
const propName = utils.stringifyPropertyKey(name);
14181390
methods.push(`${propName}: ${body}`);
14191391

1420-
const descriptorModifier = getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, "regular");
1392+
const descriptorModifier =
1393+
utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, "regular");
14211394
if (descriptorModifier === undefined) {
14221395
continue;
14231396
}

0 commit comments

Comments
 (0)