Difference between revisions of "WebAssembly/DOM"

From Lazarus wiki
Jump to navigationJump to search
 
(44 intermediate revisions by 2 users not shown)
Line 1: Line 1:
= Accessing JS Objects from WebAssembly =
+
Accessing JS Objects from [[WebAssembly/Compiler|WebAssembly]].
 +
 
 +
= JOB =
  
 
JS Object Bridge - JOB
 
JS Object Bridge - JOB
  
== General architecture ==
+
JOB provides:
Create pascal units, containing ‘proxy’ classes: calling a method on a proxy class will call the corresponding class in JS.  
+
* Units to communicate between fpc wasm and pas2js browser to call JS functions, get and set JS properties and set callbacks.
The proxy classes can be generated by adapting the existing webidl2pas tool.
+
* Units for fpc wasm with common browser classes.
 +
* A tool webidl2pas to help generating new units for JS classes from webidls.
 +
 
 +
== Using JOB ==
 +
 
 +
A JOB program has a webassembly program (fpc wasi) and a browser (pas2js) program.
 +
 
 +
The browser side contains the html and registers all global JS variables needed by the webassembly side.
 +
 
 +
See the pas2js demo/wasienv/button/BrowserButton1.lpi
 +
 
 +
https://gitlab.com/freepascal.org/fpc/pas2js/-/tree/main/demo/wasienv/button
 +
 
 +
=== JS Classes ===
 +
 
 +
Each JS class like '''HTMLButtonElement''' have a FPC class (''TJSHTMLButtonElement'') with a reference counted interface (''IJSHTMLButtonElement'').
 +
 
 +
Normally functions return an interface and expect interfaces as arguments.
 +
 
 +
=== Callbacks ===
 +
 
 +
An event handler in WebAssembly can be called from Javascript.
 +
 
 +
At the moment only methods (of object) are supported.
 +
 
 +
<source lang="pascal">
 +
  TWasmApp = class
 +
    ...
 +
    function OnButtonClick(Event: IJSEvent): boolean;
 +
    ...
 +
  end;
 +
 
 +
function TWasmApp.OnButtonClick(Event: IJSEvent): boolean;
 +
begin
 +
  JSWindow.Alert('You triggered TWasmApp.OnButtonClick');
 +
  Result:=true;
 +
end;
 +
...
 +
  JSButton.addEventListener('click',@OnButtonClick);
 +
...
 +
</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 requires creating a new bridge object using TJSHTMLElement.Cast:
 +
  HTMLElem := TJSHTMLElement.Cast(Elem);
 +
  // Note: since Elem and HTMLElem are reference counted interfaces, the compiler automatically frees temporary objects.
 +
end;
 +
</source>
 +
 
 +
This '''HTMLElement''' has the same '''ObjectId''' as '''Elem''', but it does not own it. It merely keeps a reference to the '''Elem'''. When all references are released, the JS object is released,
 +
 
 +
=== Typeof ===
 +
 
 +
'''InvokeJSTypeOf'''
 +
 
 +
See the JOBResult_* constants in unit JOB_Shared.
 +
 
 +
 
 +
== Create JOB units using webidl2pas ==
 +
 
 +
Compile the latest and greatest webidl2pas utility from fpc main:
 +
 
 +
utils/pas2js/webidl2pas.lpi
 +
 
 +
Download some webidls for example from
 +
https://hg.mozilla.org/mozilla-central/raw-file/tip/dom/webidl/<JSClassName>
 +
 
 +
Concatenate them into one file ''Foo.webidl''.
 +
 
 +
Create a text file ''FooAlias.txt'' containing the used classes from other units:
 +
<pre>
 +
Object=IJSObject
 +
Set=IJSSet
 +
Map=IJSMap
 +
Function=IJSFunction
 +
Date=IJSDate
 +
RegExp=IJSRegExp
 +
String=IJSString
 +
Array=IJSArray
 +
ArrayBuffer=IJSArrayBuffer
 +
TypedArray=IJSTypedArray
 +
BufferSource=IJSBufferSource
 +
DataView=IJSDataView
 +
JSON=IJSJSON
 +
Error=IJSError
 +
TextEncoder=IJSTextEncode
 +
TextDecoder=IJSTextDecoder
 +
</pre>
 +
 
 +
Create a text file ''FooGlobals.txt'' containing all the global JS variables in form Pascal variable name=JS class name,registered name:
 +
<pre>
 +
JSFoo=Foo,foo
 +
</pre>
 +
 
 +
Run the tool:
 +
webidl2pas -f wasmjob -i Foo.webidl --typealiases=@FooAlias.txt --globals=@FooGlobals.txt
 +
 
 +
If the tool stops, because it can not find an identifier or something is not yet supported, you can add another webidl or comment the problematic definition. Then run the tool again.
 +
 
 +
In the browser side you must register the global JS variables:
 +
<source lang="pascal">
 +
  FWADomBridge.RegisterGlobalObject(foo,'foo');
 +
</source>
 +
 
 +
=== Not yet supported webidl elements ===
 +
 
 +
==== Elements from other Pascal units ====
 +
 
 +
At the moment you can only provide a list of type aliases for classes. This does not work for callbacks, arrays, sequences, etc. It would be better to give used units and parse them.
 +
 
 +
==== callback interfaces ====
 +
 
 +
"callback interfaces" are a legacy definition.
 +
Remedy: Replace them manually with a callback
 +
 
 +
==== function returning a Dictionary ====
 +
 
 +
ToDo: Define dictionary as class and interface and return an interface reference.
 +
 
 +
==== function returning a typed sequence ====
 +
 
 +
At the moment it returns an untyped IJSArray.
 +
 
 +
ToDo: returned a typed array.
 +
 
 +
==== function returning a callback ====
 +
 
 +
==== passing an array as argument ====
 +
 
 +
==== varargs ====
 +
 
 +
Functions allowing to pass an arbitrary number of arguments.
 +
 
 +
Workaround: User can call the InvokeX function directly.
 +
 
 +
==== constructor ====
 +
 
 +
==== getter ====
 +
 
 +
==== callback property ====
 +
 
 +
A property with a callback, e.g. '''onAbort'''
 +
 
 +
At the moment it is only added as a comment. Reading callbacks is not yet supported. Theoretically it could be added as a write only property.
 +
 
 +
== JOB architecture ==
 +
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 the existing '''webidl2pas''' tool with '''-f wasmjob''' flag.
  
 
=== Data transfer between JS/WebAssembly ===
 
=== Data transfer between JS/WebAssembly ===
Line 11: Line 169:
  
 
'''Solution''':
 
'''Solution''':
 +
* Global objects like '''document''' and '''window''' are registered and queried by name.
 
* Every object is stored in an array with ID: TJOBObjectID
 
* Every object is stored in an array with ID: TJOBObjectID
 
* ID is used to pass references to object between JS and Webassembly
 
* ID is used to pass references to object between JS and Webassembly
 
* Lifetime is controlled from WebAssembly.  
 
* Lifetime is controlled from WebAssembly.  
* By using interfaces, the lifetime of objects can be controlled by the compiler.
+
* By using interfaces, the lifetime of objects are controlled by the compiler.
 
* Methods can be called using an invoke mechanism.  
 
* 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.
+
* Due to limited type support in Javascript, only a handful of types must be supported by invoke, e.g. undefined, null, boolean, number, unicodestring, Object, callbacks and the union JSValue.
  
 
=== Function Arguments ===
 
=== Function Arguments ===
Line 25: Line 184:
 
* integers (limited to double, because all numbers in JS are double, so up to 54 bits)
 
* integers (limited to double, because all numbers in JS are double, so up to 54 bits)
 
* double
 
* double
* nil
+
* nil or Variants.Null
* string (utf8)
+
* string (utf8 converted to utf16, either use UTF8Encode/UTF8Decode or install a widestringmanager by using the units unicodeducet, unicodedata, fpwidestring)
 
* unicodestring
 
* unicodestring
 
* widestring
 
* widestring
* PChar - using strlen to get the size
+
* 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
 
* TJSObject and IJSObject - its ObjectID is passed to the JS side, where the corresponding JS object is used
* JSUndefined
+
* Variant
 +
* Variants.UnAssigned or JSUndefined
 +
* TJOB_JSValue - at the moment needed for TJOB_Method.
 +
 
 +
=== 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 ''InvokeJSVariantResult''.
 +
 
 +
=== Variants ===
 +
 
 +
When an argument or result type does not have a simple type, webidl2pas uses '''Variant'''. Variants have a few newbie traps:
 +
 
 +
For JS '''undefined''' you can use '''Variants.Unassigned''' and ''VarIsEmpty(v)''.
 +
 
 +
For JS '''null''' you cannot use ''nil'', you can use '''Variants.Null''' or ''VarIsNull(v)''. ''nil'' cannot be used, because fpc treats nil with variants like an empty Unicodestring. E.g. "if aVariant=nil then" is wrong, use "if aVariant=Variants.Null then" instead.
 +
 
 +
Variants do not support 8bit strings, only 16bit Unicodestrings. For example: ''ansistring:=aVariant'' works only for ascii strings, but for unicodestrings you must either use '''UTF8Encode''' or a proper widestringmanager.
 +
 
 +
'''Interfaces''': When a function returns a Variant containing an object, the variant will contain a '''IJSObject or Variants.Null'''. Note that ''SomeIntf:=aVariant'' will compile, but does no type check, so ''SomeIntf'' might only be a ''IJSObject'' instead of ''IJSSomeIntf''. If you know that ''aVariant'' has a ''IJSSomeIntf'' you can use a type cast: '''SomeIntf:=TJSSomeIntf.Cast(aVariant);'''. There is no general way in JS to check what "class" a JS object is.
  
 
=== 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:  
+
 
 +
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:
 +
 
 +
<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;
  
all that is needed is to pass the object pointer & method pointer (both integers), plus the ID of the event object.
+
  TJSEventTarget = class(TJSObject,IJSEventTarget)
Pointers to methods & instances can be passed between JS and webassembly, this can be used.
+
    procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
Alternatively: Using the FPC dispatchstr mechanism, the correct method can be called in Webassembly. To be checked.
+
  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>
  
Advantage of this method is that only a couple of webassembly and Javascript exports are needed.
+
See units job_js and job_web for more examples.
  
 
== Implementation details ==
 
== Implementation details ==
Line 48: Line 280:
 
Here are some technical notes describing the various architectural decisions.
 
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 webidl2pas tool was extended to generate an interface from the .webidl files (-f wasmjob). These files for example exist in
 
the mozilla firefox repo on github: [https://github.com/mozilla/gecko-dev/tree/master/dom/webidl WebIDL]
 
the mozilla firefox repo on github: [https://github.com/mozilla/gecko-dev/tree/master/dom/webidl WebIDL]
  
Line 66: Line 298:
 
   TJSObject = class(TInterfacedObject,IJSObject)
 
   TJSObject = class(TInterfacedObject,IJSObject)
 
   public
 
   public
     constructor CreateFromID(aID: TJOBObjectID); virtual;
+
     constructor JOBCast(Intf: IJSObject); overload;
 +
    constructor JOBCreateFromID(aID: TJOBObjectID); virtual; // use this only for the owner (it will release it on free)
 +
    constructor JOBCreateGlobal(const aID: UnicodeString); virtual;
 +
    class function Cast(Intf: IJSObject): IJSObject; overload;
 
     destructor Destroy; override;
 
     destructor Destroy; override;
 +
    property JOBObjectID: TJOBObjectID read FJOBObjectID;
 +
    property JOBObjectIDOwner: boolean read FJOBObjectIDOwner write FJOBObjectIDOwner;
 +
    property JOBCastSrc: IJSObject read FJOBCastSrc; // nil means it is the original, otherwise it is a typecast
 
     property ObjectID: TJOBObjectID read FObjectID;
 
     property ObjectID: TJOBObjectID read FObjectID;
 
     // call a function
 
     // call a function
     procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeSetType = jisCall); virtual;
+
     procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall); virtual;
     function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Boolean; virtual;
+
     function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Boolean; virtual;
     function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): Double; virtual;
+
     function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Double; virtual;
     function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): UnicodeString; virtual;
+
     function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): UnicodeString; virtual;
     function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeGetType = jigCall): TJSObject; virtual;
+
     function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeType = jiCall): TJSObject; virtual;
     function InvokeJSValueResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeGetType = jigCall): TJOB_JSValue; virtual;
+
     function InvokeJSVariantResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Variant; virtual;
     function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): String; virtual;
+
     function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): String; virtual;
     function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeGetType = jigCall): LongInt; virtual;
+
     function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): LongInt; virtual;
 +
    function InvokeJSMaxIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): int64; virtual;
 +
    function InvokeJSTypeOf(const aName: string; Const Args: Array of const): TJOBResult; virtual;
 
     // read a property
 
     // read a property
 
     function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
 
     function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
Line 85: Line 325:
 
     function ReadJSPropertyUtf8String(const aName: string): string; virtual;
 
     function ReadJSPropertyUtf8String(const aName: string): string; virtual;
 
     function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
 
     function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
     function ReadJSPropertyValue(const aName: string): TJOB_JSValue; virtual;
+
     function ReadJSPropertyInt64(const aName: string): Int64; virtual;
 +
    function ReadJSPropertyVariant(const aName: string): Variant; virtual;
 
     // write a property
 
     // write a property
 
     procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
 
     procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
Line 91: Line 332:
 
     procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
 
     procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
 
     procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
 
     procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
     procedure WriteJSPropertyObject(const aName: string; Value: TJSObject); virtual;
+
     procedure WriteJSPropertyObject(const aName: string; Value: IJSObject); virtual;
 
     procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
 
     procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
 +
    procedure WriteJSPropertyInt64(const aName: string; Value: Int64); virtual;
 +
    procedure WriteJSPropertyVariant(const aName: string; const Value: Variant); virtual;
 
     // create a new object using the new-operator
 
     // create a new object using the new-operator
 
     function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
 
     function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
Line 109: Line 352:
 
The result is checked for the requested type and then returned to the wasm.
 
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.  
+
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 ID is returned to the webassembly, which will use the ID to create a ''TJSObject'' descendent.
  
The destructor of '''TJSObject''' calls a '''release_object''' function in javascript if the '''ObjectID''' is positive. The '''release_object''' function simply sets '''FLocalObjects['id']''' to
+
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)
+
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.
Line 151: Line 394:
  
 
* read/write array elements
 
* read/write array elements
* callbacks
+
* store/cache callbacks to support removeEventListener
 +
* move job_web+job_js units to fpc
 +
* webidl2pas:
 +
** stringifier
 +
** check Getter+Setter name conflicts
 +
** argument of sequence<something>
 +
** overloaded version for dictionary args
 +
** varargs
 +
** js getter
 +
** js constructor
 +
* pas2jsdsgn: new wasmjob browser project
 +
 
 +
[[Category:WebAssembly]]

Latest revision as of 20:01, 1 September 2022

Accessing JS Objects from WebAssembly.

JOB

JS Object Bridge - JOB

JOB provides:

  • Units to communicate between fpc wasm and pas2js browser to call JS functions, get and set JS properties and set callbacks.
  • Units for fpc wasm with common browser classes.
  • A tool webidl2pas to help generating new units for JS classes from webidls.

Using JOB

A JOB program has a webassembly program (fpc wasi) and a browser (pas2js) program.

The browser side contains the html and registers all global JS variables needed by the webassembly side.

See the pas2js demo/wasienv/button/BrowserButton1.lpi

https://gitlab.com/freepascal.org/fpc/pas2js/-/tree/main/demo/wasienv/button

JS Classes

Each JS class like HTMLButtonElement have a FPC class (TJSHTMLButtonElement) with a reference counted interface (IJSHTMLButtonElement).

Normally functions return an interface and expect interfaces as arguments.

Callbacks

An event handler in WebAssembly can be called from Javascript.

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

  TWasmApp = class
    ...
    function OnButtonClick(Event: IJSEvent): boolean;
    ...
  end;

function TWasmApp.OnButtonClick(Event: IJSEvent): boolean;
begin
  JSWindow.Alert('You triggered TWasmApp.OnButtonClick');
  Result:=true;
end;
...
  JSButton.addEventListener('click',@OnButtonClick);
...

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 requires creating a new bridge object using TJSHTMLElement.Cast:
  HTMLElem := TJSHTMLElement.Cast(Elem);
  // Note: since Elem and HTMLElem are reference counted interfaces, the compiler automatically frees temporary objects.
end;

This HTMLElement has the same ObjectId as Elem, but it does not own it. It merely keeps a reference to the Elem. When all references are released, the JS object is released,

Typeof

InvokeJSTypeOf

See the JOBResult_* constants in unit JOB_Shared.


Create JOB units using webidl2pas

Compile the latest and greatest webidl2pas utility from fpc main:

utils/pas2js/webidl2pas.lpi

Download some webidls for example from https://hg.mozilla.org/mozilla-central/raw-file/tip/dom/webidl/<JSClassName>

Concatenate them into one file Foo.webidl.

Create a text file FooAlias.txt containing the used classes from other units:

Object=IJSObject
Set=IJSSet
Map=IJSMap
Function=IJSFunction
Date=IJSDate
RegExp=IJSRegExp
String=IJSString
Array=IJSArray
ArrayBuffer=IJSArrayBuffer
TypedArray=IJSTypedArray
BufferSource=IJSBufferSource
DataView=IJSDataView
JSON=IJSJSON
Error=IJSError
TextEncoder=IJSTextEncode
TextDecoder=IJSTextDecoder

Create a text file FooGlobals.txt containing all the global JS variables in form Pascal variable name=JS class name,registered name:

JSFoo=Foo,foo

Run the tool:

webidl2pas -f wasmjob -i Foo.webidl --typealiases=@FooAlias.txt --globals=@FooGlobals.txt

If the tool stops, because it can not find an identifier or something is not yet supported, you can add another webidl or comment the problematic definition. Then run the tool again.

In the browser side you must register the global JS variables:

  FWADomBridge.RegisterGlobalObject(foo,'foo');

Not yet supported webidl elements

Elements from other Pascal units

At the moment you can only provide a list of type aliases for classes. This does not work for callbacks, arrays, sequences, etc. It would be better to give used units and parse them.

callback interfaces

"callback interfaces" are a legacy definition. Remedy: Replace them manually with a callback

function returning a Dictionary

ToDo: Define dictionary as class and interface and return an interface reference.

function returning a typed sequence

At the moment it returns an untyped IJSArray.

ToDo: returned a typed array.

function returning a callback

passing an array as argument

varargs

Functions allowing to pass an arbitrary number of arguments.

Workaround: User can call the InvokeX function directly.

constructor

getter

callback property

A property with a callback, e.g. onAbort

At the moment it is only added as a comment. Reading callbacks is not yet supported. Theoretically it could be added as a write only property.

JOB architecture

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 the existing webidl2pas tool with -f wasmjob flag.

Data transfer between JS/WebAssembly

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

Solution:

  • Global objects like document and window are registered and queried by name.
  • 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 are 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, e.g. undefined, null, boolean, number, unicodestring, Object, callbacks and the union JSValue.

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 or Variants.Null
  • string (utf8 converted to utf16, either use UTF8Encode/UTF8Decode or install a widestringmanager by using the units unicodeducet, unicodedata, fpwidestring)
  • 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
  • Variant
  • Variants.UnAssigned or JSUndefined
  • TJOB_JSValue - at the moment needed for TJOB_Method.

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 InvokeJSVariantResult.

Variants

When an argument or result type does not have a simple type, webidl2pas uses Variant. Variants have a few newbie traps:

For JS undefined you can use Variants.Unassigned and VarIsEmpty(v).

For JS null you cannot use nil, you can use Variants.Null or VarIsNull(v). nil cannot be used, because fpc treats nil with variants like an empty Unicodestring. E.g. "if aVariant=nil then" is wrong, use "if aVariant=Variants.Null then" instead.

Variants do not support 8bit strings, only 16bit Unicodestrings. For example: ansistring:=aVariant works only for ascii strings, but for unicodestrings you must either use UTF8Encode or a proper widestringmanager.

Interfaces: When a function returns a Variant containing an object, the variant will contain a IJSObject or Variants.Null. Note that SomeIntf:=aVariant will compile, but does no type check, so SomeIntf might only be a IJSObject instead of IJSSomeIntf. If you know that aVariant has a IJSSomeIntf you can use a type cast: SomeIntf:=TJSSomeIntf.Cast(aVariant);. There is no general way in JS to check what "class" a JS object is.

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;

See units job_js and job_web for more examples.

Implementation details

Here are some technical notes describing the various architectural decisions.

The webidl2pas tool was extended to generate an interface from the .webidl files (-f wasmjob). 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 JOBCast(Intf: IJSObject); overload;
    constructor JOBCreateFromID(aID: TJOBObjectID); virtual; // use this only for the owner (it will release it on free)
    constructor JOBCreateGlobal(const aID: UnicodeString); virtual;
    class function Cast(Intf: IJSObject): IJSObject; overload;
    destructor Destroy; override;
    property JOBObjectID: TJOBObjectID read FJOBObjectID;
    property JOBObjectIDOwner: boolean read FJOBObjectIDOwner write FJOBObjectIDOwner;
    property JOBCastSrc: IJSObject read FJOBCastSrc; // nil means it is the original, otherwise it is a typecast
    property ObjectID: TJOBObjectID read FObjectID;
    // call a function
    procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall); virtual;
    function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Boolean; virtual;
    function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Double; virtual;
    function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): UnicodeString; virtual;
    function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeType = jiCall): TJSObject; virtual;
    function InvokeJSVariantResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Variant; virtual;
    function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): String; virtual;
    function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): LongInt; virtual;
    function InvokeJSMaxIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): int64; virtual;
    function InvokeJSTypeOf(const aName: string; Const Args: Array of const): TJOBResult; 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 ReadJSPropertyInt64(const aName: string): Int64; virtual;
    function ReadJSPropertyVariant(const aName: string): Variant; 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: IJSObject); virtual;
    procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
    procedure WriteJSPropertyInt64(const aName: string; Value: Int64); virtual;
    procedure WriteJSPropertyVariant(const aName: string; const Value: Variant); 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
  • move job_web+job_js units to fpc
  • webidl2pas:
    • stringifier
    • check Getter+Setter name conflicts
    • argument of sequence<something>
    • overloaded version for dictionary args
    • varargs
    • js getter
    • js constructor
  • pas2jsdsgn: new wasmjob browser project