Difference between revisions of "WebAssembly/DOM"

From Lazarus wiki
Jump to navigationJump to search
(Created page with "== Accessing the DOM from WebAssembly == === General architecture === Create pascal units, containing ‘proxy’ classes: calling a method on a proxy class will call the co...")
 
 
(26 intermediate revisions by 3 users not shown)
Line 1: Line 1:
== Accessing the DOM from WebAssembly ==
+
= Accessing JS Objects from WebAssembly =
  
=== General architecture ===  
+
JS Object Bridge - JOB
 +
 
 +
== General architecture ==
 
Create pascal units, containing ‘proxy’ classes: calling a method on a proxy class will call the corresponding class in JS.  
 
Create pascal units, containing ‘proxy’ classes: calling a method on a proxy class will call the corresponding class in JS.  
 
The proxy classes can be generated by adapting the existing webidl2pas tool.
 
The proxy classes can be generated by adapting the existing webidl2pas tool.
  
==== Problem: data ====
+
=== Data transfer between JS/WebAssembly ===
JS/Webassembly interface only supports passing integers & floats, not objects.
+
JS/Webassembly interface only supports passing atomic types like boolean, integers and floats, not objects or strings.
 +
 
 +
'''Solution''':
 +
* Every object is stored in an array with ID: TJOBObjectID
 +
* ID is used to pass references to object between JS and Webassembly
 +
* Lifetime is controlled from WebAssembly.
 +
* By using interfaces, the lifetime of objects can be controlled by the compiler.
 +
* Methods can be called using an invoke mechanism.
 +
* Due to limited type support in Javascript, only a handful of types must be supported by invoke.
 +
 
 +
=== Function Arguments ===
 +
 
 +
When calling a JS function from wasm, you can pass the following types/constants:
 +
 
 +
* boolean
 +
* integers (limited to double, because all numbers in JS are double, so up to 54 bits)
 +
* double
 +
* nil
 +
* string (utf8 converted to utf16)
 +
* unicodestring
 +
* widestring
 +
* PChar - using strlen to get the size and utf8 converted to utf16
 +
* PWideChar - using strlen to get the size
 +
* TJSObject and IJSObject - its ObjectID is passed to the JS side, where the corresponding JS object is used
 +
* JSUndefined
 +
* TJOB_JSValue
 +
 
 +
=== Function Result ===
 +
 
 +
Calling a JS function is done via the ''InvokeJS*Result'' functions, e.g. ''aJSDate.InvokeJSUnicodeStringResult('toLocaleDateString',[])'' which returns a ''UnicodeString''.
 +
 
 +
If the function does not exist, an ''EJSInvoke'' exception is raised.
 +
 
 +
If the function returns the JS undefined value, JOB returns the default value, e.g. InvokeJSUnicodeStringResult returns the empty string, InvokeJSDoubleResult returns NaN, InvokeJSObjectResult returns nil, InvokeJSBooleanResult returns false.
 +
 
 +
If the function returns an incompatible type, e.g. InvokeJSUnicodeStringResult returns a number, an ''EJSInvoke'' exception is raised.
  
'''solution''': Every object is stored in an array with ID
+
To retrieve any kind of JS value, use InvokeJSValueResult.
ID is used to pass references to object between JS and Webassembly
 
Lifetime is controlled from WebAssembly.
 
By using interfaces, the lifetime of objects can be controlled by compiler.
 
Methods can be called using an invoke mechanism. Due to limited type support in Javascript, only a handful of types must be supported by invoke.
 
  
==== Problem: Callbacks ====
+
=== Callbacks ===
  
An event handler in WebAssembly must be callable from Javascript.  
+
An event handler in WebAssembly can be called from Javascript.  
The '''AddEventListener''' has a single method signature, so a single exported function from webassembly can be used for this:
 
  
all that is needed is to pass the object pointer & method pointer (both integers), plus the ID of the event object.
+
At the moment only methods (of object) are supported.
Pointers to methods & instances can be passed between JS and webassembly, this can be used.
 
Alternatively: Using the FPC dispatchstr mechanism, the correct method can be called in Webassembly. To be checked.
 
  
Advantage of this method is that only a couple of webassembly and Javascript exports are needed.
+
Every function type needs a callback, which decodes the arguments and encode the result.  
  
=== Implementation details ===
+
For example TJSEventHandler:
 +
 
 +
<source lang="pascal">
 +
type
 +
  TJSEventHandler = function(Event: IJSEventListenerEvent): boolean of object;
 +
...
 +
function JOBCallTJSEventHandler(const aMethod: TMethod; var H: TJOBCallbackHelper): PByte;
 +
var
 +
  Event: IJSEventListenerEvent;
 +
begin
 +
  // get arguments. First as IJSEventListenerEvent
 +
  Event:=H.GetObject(TJSEventListenerEvent) as IJSEventListenerEvent;
 +
  // call the method and encode the result
 +
  Result:=H.AllocBool(TJSEventHandler(aMethod)(Event));
 +
end;
 +
</source>
 +
 
 +
TJOBCallbackHelper provides functions to decode arguments and encode the result.
 +
 
 +
Passing a method as argument works like this:
 +
 
 +
<source lang="pascal">
 +
type
 +
  IJSEventTarget = interface
 +
    ['{1883145B-C826-47D1-9C63-47546BA536BD}']
 +
    procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
 +
  end;
 +
 
 +
  TJSEventTarget = class(TJSObject,IJSEventTarget)
 +
    procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
 +
  end;
 +
...
 +
procedure TJSEventTarget.addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
 +
var
 +
  m: TJOB_JSValueMethod;
 +
begin
 +
  // combine the users method and the callback into one argument m
 +
  m:=TJOB_JSValueMethod.Create(TMethod(aListener),@JOBCallTJSEventHandler);
 +
  try
 +
    // call the JS function addEventListener(aName,m)
 +
    InvokeJSNoResult('addEventListener',[aName,m]);
 +
  finally
 +
    m.Free;
 +
  end;
 +
end;
 +
</source>
 +
 
 +
=== Type casts ===
 +
 
 +
Often a low level interface needs to be type casted to a descendant (or another type). For example ''JSDocument.getElementById'' returns a IJSElement, which is type casted to IJSHTMLElement:
 +
 
 +
<source lang="pascal">
 +
var
 +
  Elem: IJSElement;
 +
  HTMLElem: IJSHTMLElement;
 +
begin
 +
  Elem := JSDocument.getElementById('button');
 +
  // type casting to IJSHTMLElement require creating a special TJSHTMLElement:
 +
  HTMLElem := TJSHTMLElement.Cast(Elem) as IJSHTMLElement;
 +
end;
 +
</source>
 +
 
 +
This special TJSHTMLElement has the same ObjectId, but it does not own it. It merely keeps an interface reference to the original IJSObject. When all interface references are released, the JS object is released,
 +
 
 +
=== Typeof ===
 +
 
 +
'''InvokeJSTypeOf'''
 +
 
 +
See the JOBResult_* constants in unit JOB_Shared.
 +
 
 +
== Implementation details ==
  
 
Here are some technical notes describing the various architectural decisions.
 
Here are some technical notes describing the various architectural decisions.
Line 33: Line 133:
  
 
<source lang="pascal">
 
<source lang="pascal">
IElement = interface ['someawfulGUID'] (IJSObject);
+
IJSElement = interface(IJSObject)
 +
  ['someawfulGUID']
 
   function childElementCount : Integer;
 
   function childElementCount : Integer;
   function firstElementChild : IElement;
+
   function firstElementChild : IJSElement;
 
   // all other
 
   // all other
 
end;
 
end;
 
</source>
 
</source>
 
Only the interfaces are exposed in the API to access the DOM.
 
  
 
In implementation, the following kind of code can be found:
 
In implementation, the following kind of code can be found:
Line 46: Line 145:
 
<source lang="pascal">
 
<source lang="pascal">
 
// Hand crafted in e.g. JSObject unit
 
// Hand crafted in e.g. JSObject unit
TJSObject = class(TInterfacedObject)
+
  TJSObject = class(TInterfacedObject,IJSObject)
begin
+
  public
  constructor CreateFromID(aID: NativeInt);
+
    constructor CreateFromID(aID: TJOBObjectID); virtual;
  destructor destroy; override;
+
    destructor Destroy; override;
  Property ObjectID : NativeInt;
+
    property ObjectID: TJOBObjectID read FObjectID;
  function InvokeJSNativeIntResult(aName : string; Const args : Array of const) : NativeInt;
+
    // call a function
  function InvokeJSStringResult(aName : string; Const args : Array of const) : String;
+
    procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeSetType = jisCall); virtual;
  function InvokeJSObjResult(aName : string; aResultClass: TDOMOBjectClass; Const args : Array of const) : TDDOMOBject;
+
    function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Boolean; virtual;
end;
+
    function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Double; virtual;
 +
    function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): UnicodeString; virtual;
 +
    function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeGetType = jigCall): TJSObject; virtual;
 +
    function InvokeJSValueResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): TJOB_JSValue; virtual;
 +
    function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): String; virtual;
 +
    function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): LongInt; virtual;
 +
    // read a property
 +
    function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
 +
    function ReadJSPropertyDouble(const aName: string): double; virtual;
 +
    function ReadJSPropertyUnicodeString(const aName: string): UnicodeString; virtual;
 +
    function ReadJSPropertyObject(const aName: string; aResultClass: TJSObjectClass): TJSObject; virtual;
 +
    function ReadJSPropertyUtf8String(const aName: string): string; virtual;
 +
    function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
 +
    function ReadJSPropertyValue(const aName: string): TJOB_JSValue; virtual;
 +
    // write a property
 +
    procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
 +
    procedure WriteJSPropertyDouble(const aName: string; Value: Double); virtual;
 +
    procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
 +
    procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
 +
    procedure WriteJSPropertyObject(const aName: string; Value: TJSObject); virtual;
 +
    procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
 +
    // create a new object using the new-operator
 +
    function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
 +
  end;
 
</source>
 
</source>
  
The various '''Invoke*''' functions encode the arguments in a memory block so they can be
+
The various '''Invoke*''' functions encode the arguments in a memory block so they can be read on the JS side, then calls a '''Invoke_*Result''' function which lives in
read on the JS side, then calls a JSInvokeNNN function which lives in
 
 
Javascript, and which is imported from the browser.  
 
Javascript, and which is imported from the browser.  
  
Line 64: Line 185:
  
 
* Negative IDs are special: window, document.
 
* Negative IDs are special: window, document.
* positive IDs use a '''wasmObjects['id']''' to look for the object.
+
* positive IDs are temporary objects created via the '''InvokeJSObjectResult''', see below.
  
The '''Invoke*''' function decodes the arguments and uses '''TJSFunction.apply''' to execute the requested function.
+
The '''Invoke_*Result''' pas2js function decodes the arguments and uses '''TJSFunction.apply''' to execute the requested function.
The result is put in a memory block, encoded in the same way as incoming arguments.
+
The result is checked for the requested type and then returned to the wasm.
  
If the result is an object, an ID is generated (simple counter), the result
+
If the result is an object, an ID is generated (simple counter), the result value is stored in an array '''FLocalObjects'''.  
value is stored in the array '''wasmObjects['id']''' .  
 
  
The ID is returned to the webassembly, which will use the ID to create a TJSObject descendent.
+
The ID is returned to the webassembly, which will use the ID to create a ''TJSObject'' descendent.
  
The destructor of '''TJSObject''' calls a '''releaseObject''' function in javascript
+
The destructor of '''TJSObject''' calls a '''__job_release_object''' function in javascript if the '''ObjectID''' is positive. The '''ReleaseObject''' function simply sets '''FLocalObjects[id]''' to
if the '''ObjectID''' is positive. The '''releaseObject''' function simply sets '''wasmObjects['id']''' to
+
null, so the browser also releases it.
null. (so the browser also releases it)
 
  
 
The above is a basic invoke mechanism for Javascript code.
 
The above is a basic invoke mechanism for Javascript code.
  
This basic mechanism is then used by a modified version of the webidl
+
This basic mechanism is then used by a modified version of the webidl program to generate proxy definitions. For each object in Javascript, 2 definitions are generated:  
program to generate proxy definitions. For each object in Javascript, 2
 
definitions are generated:  
 
  
 
* The interface (see above for an example)
 
* The interface (see above for an example)
* An implementation object as below, descendent of '''TJSObject'''
+
* An implementation object as below, descendant of '''TJSObject'''
  
 
<source lang="pascal">
 
<source lang="pascal">
 
// Generated from webIDL in jsweb/jsdom unit.
 
// Generated from webIDL in jsweb/jsdom unit.
 
   
 
   
TElementImpl = class(TJSObject,IElement)
+
IJSElement = interface(IJSNode)
 
   function childElementCount : Integer;
 
   function childElementCount : Integer;
   function firstElementChild : IElement;
+
   function firstElementChild : IJSElement;
 +
end;
 +
 
 +
TJSElementImpl = class(TJSObject,IJSElement)
 +
  function childElementCount : Integer;
 +
  function firstElementChild : IJSElement;
 
   // all other
 
   // all other
 
end;
 
end;
 
   
 
   
+
function TJSElementImpl.childElementCount : Integer;
function TElementImpl.childElementCount : Integer;
 
 
begin
 
begin
   Result:=InvokeJSStringResult('childElementCount',[]).AsInteger;
+
   Result:=ReadJSPropertyLongInt('childElementCount');
 
end;
 
end;
 
   
 
   
function TElementImpl.firstElementChild : IElement;
+
function TJSElementImpl.firstElementChild : IJSElement;
 
begin
 
begin
   Result:=InvokeJSObjResult('firstElementChild',TElementImpl,[]) as IElement;
+
   Result:=ReadJSPropertyObject('firstElementChild',TJSElementImpl) as IJSElement;
 
end;
 
end;
 
</source>
 
</source>
 +
 +
=ToDos=
 +
 +
* read/write array elements
 +
* store/cache callbacks to support removeEventListener
 +
* extend webidl2pas to produce code for wasm-job
 +
 +
[[Category:WebAssembly]]

Latest revision as of 17:54, 27 June 2022

Accessing JS Objects from WebAssembly

JS Object Bridge - JOB

General architecture

Create pascal units, containing ‘proxy’ classes: calling a method on a proxy class will call the corresponding class in JS. The proxy classes can be generated by adapting the existing webidl2pas tool.

Data transfer between JS/WebAssembly

JS/Webassembly interface only supports passing atomic types like boolean, integers and floats, not objects or strings.

Solution:

  • Every object is stored in an array with ID: TJOBObjectID
  • ID is used to pass references to object between JS and Webassembly
  • Lifetime is controlled from WebAssembly.
  • By using interfaces, the lifetime of objects can be controlled by the compiler.
  • Methods can be called using an invoke mechanism.
  • Due to limited type support in Javascript, only a handful of types must be supported by invoke.

Function Arguments

When calling a JS function from wasm, you can pass the following types/constants:

  • boolean
  • integers (limited to double, because all numbers in JS are double, so up to 54 bits)
  • double
  • nil
  • string (utf8 converted to utf16)
  • unicodestring
  • widestring
  • PChar - using strlen to get the size and utf8 converted to utf16
  • PWideChar - using strlen to get the size
  • TJSObject and IJSObject - its ObjectID is passed to the JS side, where the corresponding JS object is used
  • JSUndefined
  • TJOB_JSValue

Function Result

Calling a JS function is done via the InvokeJS*Result functions, e.g. aJSDate.InvokeJSUnicodeStringResult('toLocaleDateString',[]) which returns a UnicodeString.

If the function does not exist, an EJSInvoke exception is raised.

If the function returns the JS undefined value, JOB returns the default value, e.g. InvokeJSUnicodeStringResult returns the empty string, InvokeJSDoubleResult returns NaN, InvokeJSObjectResult returns nil, InvokeJSBooleanResult returns false.

If the function returns an incompatible type, e.g. InvokeJSUnicodeStringResult returns a number, an EJSInvoke exception is raised.

To retrieve any kind of JS value, use InvokeJSValueResult.

Callbacks

An event handler in WebAssembly can be called from Javascript.

At the moment only methods (of object) are supported.

Every function type needs a callback, which decodes the arguments and encode the result.

For example TJSEventHandler:

type
  TJSEventHandler = function(Event: IJSEventListenerEvent): boolean of object;
...
function JOBCallTJSEventHandler(const aMethod: TMethod; var H: TJOBCallbackHelper): PByte;
var
  Event: IJSEventListenerEvent;
begin
  // get arguments. First as IJSEventListenerEvent
  Event:=H.GetObject(TJSEventListenerEvent) as IJSEventListenerEvent;
  // call the method and encode the result
  Result:=H.AllocBool(TJSEventHandler(aMethod)(Event));
end;

TJOBCallbackHelper provides functions to decode arguments and encode the result.

Passing a method as argument works like this:

type
  IJSEventTarget = interface
    ['{1883145B-C826-47D1-9C63-47546BA536BD}']
    procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
  end;

  TJSEventTarget = class(TJSObject,IJSEventTarget)
    procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
  end;
...
procedure TJSEventTarget.addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
var
  m: TJOB_JSValueMethod;
begin
  // combine the users method and the callback into one argument m
  m:=TJOB_JSValueMethod.Create(TMethod(aListener),@JOBCallTJSEventHandler);
  try
    // call the JS function addEventListener(aName,m)
    InvokeJSNoResult('addEventListener',[aName,m]);
  finally
    m.Free;
  end;
end;

Type casts

Often a low level interface needs to be type casted to a descendant (or another type). For example JSDocument.getElementById returns a IJSElement, which is type casted to IJSHTMLElement:

var 
  Elem: IJSElement;
  HTMLElem: IJSHTMLElement;
begin
  Elem := JSDocument.getElementById('button');
  // type casting to IJSHTMLElement require creating a special TJSHTMLElement:
  HTMLElem := TJSHTMLElement.Cast(Elem) as IJSHTMLElement;
end;

This special TJSHTMLElement has the same ObjectId, but it does not own it. It merely keeps an interface reference to the original IJSObject. When all interface references are released, the JS object is released,

Typeof

InvokeJSTypeOf

See the JOBResult_* constants in unit JOB_Shared.

Implementation details

Here are some technical notes describing the various architectural decisions.

A tool is created to generate an interface from the .webidl files. These files for example exist in the mozilla firefox repo on github: WebIDL

IJSElement = interface(IJSObject)
  ['someawfulGUID']
  function childElementCount : Integer;
  function firstElementChild : IJSElement;
  // all other
end;

In implementation, the following kind of code can be found:

// Hand crafted in e.g. JSObject unit
  TJSObject = class(TInterfacedObject,IJSObject)
  public
    constructor CreateFromID(aID: TJOBObjectID); virtual;
    destructor Destroy; override;
    property ObjectID: TJOBObjectID read FObjectID;
    // call a function
    procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeSetType = jisCall); virtual;
    function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Boolean; virtual;
    function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Double; virtual;
    function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): UnicodeString; virtual;
    function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeGetType = jigCall): TJSObject; virtual;
    function InvokeJSValueResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): TJOB_JSValue; virtual;
    function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): String; virtual;
    function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): LongInt; virtual;
    // read a property
    function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
    function ReadJSPropertyDouble(const aName: string): double; virtual;
    function ReadJSPropertyUnicodeString(const aName: string): UnicodeString; virtual;
    function ReadJSPropertyObject(const aName: string; aResultClass: TJSObjectClass): TJSObject; virtual;
    function ReadJSPropertyUtf8String(const aName: string): string; virtual;
    function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
    function ReadJSPropertyValue(const aName: string): TJOB_JSValue; virtual;
    // write a property
    procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
    procedure WriteJSPropertyDouble(const aName: string; Value: Double); virtual;
    procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
    procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
    procedure WriteJSPropertyObject(const aName: string; Value: TJSObject); virtual;
    procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
    // create a new object using the new-operator
    function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
  end;

The various Invoke* functions encode the arguments in a memory block so they can be read on the JS side, then calls a Invoke_*Result function which lives in Javascript, and which is imported from the browser.

That function does the actual call: it uses ObjectID to look for the Self object in an array:

  • Negative IDs are special: window, document.
  • positive IDs are temporary objects created via the InvokeJSObjectResult, see below.

The Invoke_*Result pas2js function decodes the arguments and uses TJSFunction.apply to execute the requested function. The result is checked for the requested type and then returned to the wasm.

If the result is an object, an ID is generated (simple counter), the result value is stored in an array FLocalObjects.

The ID is returned to the webassembly, which will use the ID to create a TJSObject descendent.

The destructor of TJSObject calls a __job_release_object function in javascript if the ObjectID is positive. The ReleaseObject function simply sets FLocalObjects[id] to null, so the browser also releases it.

The above is a basic invoke mechanism for Javascript code.

This basic mechanism is then used by a modified version of the webidl program to generate proxy definitions. For each object in Javascript, 2 definitions are generated:

  • The interface (see above for an example)
  • An implementation object as below, descendant of TJSObject
// Generated from webIDL in jsweb/jsdom unit.
 
IJSElement = interface(IJSNode)
  function childElementCount : Integer;
  function firstElementChild : IJSElement;
end;

TJSElementImpl = class(TJSObject,IJSElement)
  function childElementCount : Integer;
  function firstElementChild : IJSElement;
  // all other
end;
 
function TJSElementImpl.childElementCount : Integer;
begin
  Result:=ReadJSPropertyLongInt('childElementCount');
end;
 
function TJSElementImpl.firstElementChild : IJSElement;
begin
  Result:=ReadJSPropertyObject('firstElementChild',TJSElementImpl) as IJSElement;
end;

ToDos

  • read/write array elements
  • store/cache callbacks to support removeEventListener
  • extend webidl2pas to produce code for wasm-job