Back to Chromium

Extensions WebIDL Conversion

tools/json_schema_compiler/extensions_webidl_conversion.md

149.0.7827.216.0 KB
Original Source

Extensions WebIDL Conversion

This document describes the process for converting old style Extensions IDL schema files to modern WebIDL syntax and details rules for how to handle specific conversion cases.

Motivation/Background

For a long time Extension API schema files have used an old unmaintained IDL parser, which has fallen out of date with modern WebIDL. Recently support for a modern Blink maintained WebIDL parser was added (crbug.com/340297705), which means we are now able to start updating our IDL schema files to modern WebIDL.

Conversion Process

When converting an IDL file a new file should be created in the same location as the old schema with the same name but using a .webidl suffix instead. Once the converted file has been created, it must be verified to have no differences from the output of the original by running the tools/json_schema_compiler/web_idl_diff_tool.py utility, which takes the filename of the old and the new schema as arguments. e.g. tools/json_schema_compiler/web_idl_diff_tool.py extensions/common/api/alarms.idl extensions/common/api/alarms.webidl Only after it is verified that there is no differences by the tool outputting No difference found!, the old .idl schema file can be moved to tools/json_schema_compiler/test/converted_schemas/ along with a copy of the newly converted .webidl schema. Then tools/json_schema_compiler/web_idl_diff_tool_test.py should be updated to add these filenames to the converted_schemas array and web_idl_diff_tool_test.py should be run to verify it passes with the newly added entry. Finally, the associated schema.gni file with the old .idl filename will need to be updated to .webidl.

Conversion Rules

The following conversion rules and the provided alarms API conversion example can be used as a guide for structure, syntax, and style.

Namespace Removal

The top-level namespace declaration is removed.

  • The content of the old namespace foo { ... } becomes the new top-level interface Foo { ... }. Note the capitalization change from foo to Foo.
  • The descriptive comment above the old namespace should be moved to be above the new interface definition.
  • A partial interface Browser is added at the end of the file to expose the API on the browser object. For an API named foo, this should be:
interface Foo {
  ...
}

partial interface Browser {
  static attribute Foo foo;
};
  • If the previous namespace name contained periods, it will need to use nested partial interfaces for each pieces of the API name. For example, an API which was previously exposed under system.cpu would become:
// Note the interface name here is only the final part of the API name.
interface CPU {
  ...
}

partial interface System {
  static attribute CPU cpu;
}

partial interface Browser {
  static attribute System system;
}

Note: "Stub" manifest key schema files which just define manifest key types are an exception to this rule and do not need a top level interface or Browser definition. See the ManifestKeys Dictionary section below for more details.

Dictionaries

Dictionaries are moved outside the main interface to the top level of the file. Their internal structure remains the same. Descriptive comments above them should be moved along with them.

A key difference between the old and new formats is the default for dictionary members:

  • The old .idl format treated members as required by default. A ? suffix was used to mark a member as optional.
  • The new WebIDL format treats members as optional by default. The required keyword is used to mark a member as non-optional.

Therefore, the conversion rule is:

  • If a member in the old .idl file does not have a ? suffix, it was required and must be prefixed with required in the new .webidl file.
  • If a member in the old .idl file does have a ? suffix, it was optional and must not have a required prefix in the new .webidl file.

Example of Conversion:

Before (.idl):

dictionary MyInfo {
  // This is required by default in the old format.
  DOMString name;

  // The '?' makes this optional.
  long? age;
};

After (.webidl):

dictionary MyInfo {
  // Must be explicitly marked 'required' in the new format.
  required DOMString name;

  // Is optional by default in the new format, so no keyword is needed.
  long age;
};

Operations on Dictionaries to Callbacks

Some dictionaries previously would define static operations on them to indicate they had a callable function. These are now instead defined using callbacks, which are defined on the top level of the IDL file and then referenced as the type for the dictionary member. Any descriptive comment above the old operation definition should remain above the new dictionary member that replaced it. e.g.

dictionary AutomationEvent {
  // Function description.
  static void stopPropagation();
};

Would become:

callback StopPropagationCallback = undefined();
dictionary AutomationEvent {
  // Function description.
  required StopPropagationCallback stopPropagation;
};

ManifestKeys Dictionary

The ManifestKeys dictionary in the old schemas is a special case and needs to be handled slightly differently from other dictionaries when being converted. Like other dictionaries it is also moved to the top level of the file, but it becomes a partial dictionary and the name is changed to ExtensionManifest. e.g.

dictionary ManifestKeys {
  // Description of a manifest key.
  SomeCustomType? foobar_key;
};

Would become:

partial dictionary ExtensionManifest {
  // Description of a manifest key.
  SomeCustomType foobar_key;
};

If the file is a "stub" schema used for just defining manifest keys (i.e. with no functions or events defined), then the partial ExtensionManifest dictionary also needs a [Namespace=...] extended attribute added to it with the namespace name from the original IDL file. Any existing extended attributes and comments that were on the original IDL namespace node also need to be moved above the partial ExtensionManifest dictionary. e.g.

// `fooHandlers` manifest key definition.
[generate_error_messages] namespace fooHandlers {

  dictionary ManifestKeys {
    // Important manifest key.
    SomeCustomType? foo_key;
  };
};

Would become:

// `fooHandlers` manifest key definition.
[generate_error_messages, Namespace=fooHandlers]
partial dictionary ExtensionManifest {
  // Important manifest key.
  SomeCustomType foo_key;
};

Enums

Enums are moved outside the main interface to the top level of the file. Their internal structure remains very similar, but the values are instead defined with quoted strings e.g.

enum VendorIdSource {
  "bluetooth",
  "usb"
};

Descriptive comments above the whole enum should be moved along with them.

Typedefs for Referencing External Types or Local Aliasing

WebIDL does not allow Type names to contain periods, so any references to Types defined in other schema files (e.g. extensionTypes.FrameType or tabs.Tab) must be updated. We handle this by creating a local Typedef object at the top level of the file with a name combining the capitalized namespace and Type name, and giving it an ExternalExtensionType=] extended attribute with the original string.

Typedefs can also be used to create local aliases for types when you want to apply specific extended attributes to them, such as [instanceOf=...], which is useful when they need to be used in places where extended attributes are not normally allowed by the IDL parser (e.g. on the Type of a Promise<Type>).

If a Typedef has an ExternalExtensionType extended attribute it is treated as an external type reference; otherwise it is treated as a local alias and the underlying type of the Typedef is used, along with any extended attributes on it.

Example of External Type:

// Old .idl file
dictionary contentScripts {
  extensionTypes.RunAt? run_at;
}

Would become:

// New .webidl file
[ExternalExtensionType="extensionTypes.RunAt"]
typedef object ExtensionTypesRunAt;

dictionary contentScripts {
  ExtensionTypesRunAt run_at;
}

Example of Local Alias with instanceOf:

// Old .idl file
callback BlobCallback = void([instanceOf=Blob] object blob);

Would become:

// New .webidl file
[instanceOf=Blob]
typedef object Blob;

[requiredCallback] static Promise<Blob> getBlob();

Functions and Callbacks to Promises

All functions that used a trailing callback must be converted to return a Promise.

  • Return Type: The function's return type changes from static void to static Promise<T>.
  • Callback Removal: The final callback parameter is removed from the function's signature.
  • Promise Type T: The type T inside the Promise<T> is determined by the arguments of the old callback:
    • A callback with one argument void(Type arg) becomes Promise<Type>. For example:
      • void(Alarm alarm) becomes Promise<Alarm>.
      • void(optional DOMString foo becomes Promise<DOMString?>.
    • A callback with no arguments void() becomes Promise<undefined>.
    • A nullable argument void(optional Type arg) becomes a nullable promise type Promise<Type?>.
    • An array argument void(Type[] arg) becomes a sequence Promise<sequence<Type>>.
  • Callback Optionality: If the original callback was not marked as optional, the new definition must have a [requiredCallback] extended attribute added before the static Promise return type. e.g. [requiredCallback] Promise<boolean> checkFoo();
  • void Keyword: The void keyword should be replaced with undefined.

Promise Function Documentation

When a function is converted from using a callback to returning a Promise, its documentation must be updated to reflect this change. The single description for the old |callback| parameter is split into two new, more specific tags: |Returns| and |PromiseValue|.

  1. The |Returns| Tag: What the Function Returns This tag describes the Promise object itself. Its text should be the original description of the |callback| parameter from the function's main documentation block. It explains when the promise will be resolved.

    • Example Source: // |callback|: Called with the resulting alarm, if any.
    • Becomes: // |Returns|: Called with the resulting alarm, if any.
  2. The |PromiseValue| Tag: What the Promise Resolves With This tag describes the value inside the Promise upon successful resolution. To find this text, look at the original callback definition (e.g., callback AlarmCallback = ...). The documentation for the parameters passed to that callback becomes the content for the new |PromiseValue| tag.

    • Example Source:
      // |alarm|: The alarm that was found.
      callback AlarmCallback = void(optional Alarm alarm);
      
    • Becomes: // |PromiseValue|: alarm: The alarm that was found.

If the original callback took no arguments (e.g., void()), then no |PromiseValue| tag is needed.

Complete Example:

Before:

// Description of the function.
// |name|: The name of the alarm to get. Defaults to the empty string.
// |callback|: Called with the resulting alarm, if any.
static void get(optional DOMString name, AlarmCallback callback);

// |alarm|: The alarm that was found.
callback AlarmCallback = void(optional Alarm alarm);

After:

// Description of the function.
// |name|: The name of the alarm to get. Defaults to the empty string.
// |Returns|: Called with the resulting alarm, if any.
// |PromiseValue|: alarm: The alarm that was found.
[requiredCallback] static Promise<Alarm?> get(optional DOMString name);

Synchronous Custom Type Function Returns

If a function has a synchronous return that is either a custom type or an explicit object type, it should be marked nullable in the new definition with a ? after the type. The old IDL parser didn't support marking returns as nullable, so this was previously done under the hood for all function returns of these types.

Before:

static FooType getFoo(DOMString name);

After:

static FooType? getFoo(DOMString name);

Events

The old interface Events { ... } is replaced with a more explicit event handling interface. For each event, such as onFoo, follow these steps:

  1. Define a Listener Callback. Create a new callback definition for the event listener. The name should be OnFooListener. If the argument passed to the callback had a comment description previously, that should be moved to be directly above the new callback definition; otherwise, it does not need a comment describing that argument. Important: The descriptive comment for each argument in the old event (e.g., // |alarm|: The alarm that has elapsed.) must be moved to be directly above the new callback definition.

For example, an event with multiple parameters would be converted as follows:

Before:

// Old .idl file
interface Events {
  // Fired when something interesting happens.
  // |param1|: The first parameter.
  // |param2|: The second parameter.
  static void onFoo(DOMString param1, long param2);
};

After:

// New .webidl file

// |param1|: The first parameter.
// |param2|: The second parameter.
callback OnFooListener = undefined (DOMString param1, long param2);
  1. Define the Event Interface. Create a new interface named OnFooEvent that inherits from ExtensionEvent and includes the standard addListener, removeListener, and hasListener methods. This interface should come directly after the corresponding callback definition.
interface OnFooEvent : ExtensionEvent {
  static undefined addListener(OnFooListener listener);
  static undefined removeListener(OnFooListener listener);
  static boolean hasListener(OnFooListener listener);
};
  1. Add Event Attribute. In the main API interface (e.g., Alarms), add a static attribute for the event. The general descriptive comment for the event (e.g., "Fired when...") and any extended attributes which were on the original event definition (e.g. [maxListeners=1]) should be moved to this new attribute definition.
// Fired when something interesting happens.
static attribute OnFooEvent onFoo;

Properties

Instead of using interface Properties { ... } to define constant properties exposed on the API, these are now just defined on the main API interface definition. Additionally, instead of using Operation definitions with an extended attribute for the value, they are defined as actual consts specifying the type and value.

String values have to be handled a little bit differently, as WebIDL doesn't allow using string literals when specifying the value of a const. Instead we set the value as = 0 and use a [StringValue="Foo"] extended attribute to specify the actual string literal.

For example:

// Old .idl file
interface Properties {
  // The maximum allowed number.
  [value=100] static long MAX_NUMBER_ALLOWED();

  // An important string key.
  [value="_importantKey"] static DOMString KEY_STRING();
}

Would become:

// New .webidl file
interface FooAPI {
  // The maximum allowed number.
  const long MAX_NUMBER_ALLOWED = 100;

  // An important string key.
  [StringValue="_importantKey"] const DOMString KEY_STRING = 0;
}

Since the updated schema files are heavily modified from the previous files and not just renames, the year for the copyright notice at the top of the file should be updated to the current year.

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Would become:

// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Canonical Examples

The ./test/converted_schemas/ folder contains examples of previous conversions that can be used as reference for other syntax and formatting. An example of the alarms conversion from that folder:

Old alarms IDL: @./test/converted_schemas/alarms.idl

New alarms WebIDL: @./test/converted_schemas/alarms.webidl