Sign in Join
Wiki copytemporary

WebAssembly/DOM: what changed

Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.

Title: WebAssembly/DOM

OldNewLine
1+Accessing JS Objects from [[WebAssembly/Compiler|WebAssembly]].
2+
3+# What is JOB?
4+
5+JS Object Bridge - JOB
6+
7+JOB provides:
8+
9+- Units to communicate between fpc wasm and pas2js browser to call JS functions, get and set JS properties and set callbacks.
10+- Units for fpc wasm with common browser classes.
11+- A tool webidl2pas to help generating new units for JS classes from webidls.
12+
13+# Using JOB
14+
15+A JOB program has a webassembly program (fpc wasi) and a browser (pas2js) program.
16+
17+The browser side contains the html and registers all global JS variables needed by the webassembly side.
18+
19+See the pas2js demo/wasienv/button/BrowserButton1.lpi
20+
21+<https://gitlab.com/freepascal.org/fpc/pas2js/-/tree/main/demo/wasienv/button>
22+
23+## JS Classes
24+
25+Each JS class like **HTMLButtonElement** have a FPC class (*TJSHTMLButtonElement*) with a reference counted interface (*IJSHTMLButtonElement*).
26+
27+Normally functions return an interface and expect interfaces as arguments.
28+
29+## Callbacks
30+
31+An event handler in WebAssembly can be called from Javascript.
32+
33+At the moment only methods (of object) are supported.
34+
35+``` pascal
36+ TWasmApp = class
37+ ...
38+ function OnButtonClick(Event: IJSEvent): boolean;
39+ ...
40+ end;
41+
42+function TWasmApp.OnButtonClick(Event: IJSEvent): boolean;
43+begin
44+ JSWindow.Alert('You triggered TWasmApp.OnButtonClick');
45+ Result:=true;
46+end;
47+...
48+ JSButton.addEventListener('click',@OnButtonClick);
49+...
50+```
51+
52+## Type casts
53+
54+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:
55+
56+``` pascal
57+var
58+ Elem: IJSElement;
59+ HTMLElem: IJSHTMLElement;
60+begin
61+ Elem := JSDocument.getElementById('button');
62+ // type casting to IJSHTMLElement requires creating a new bridge object using TJSHTMLElement.Cast:
63+ HTMLElem := TJSHTMLElement.Cast(Elem);
64+ // Note: since Elem and HTMLElem are reference counted interfaces, the compiler automatically frees temporary objects.
65+end;
66+```
67+
68+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,
69+
70+## Typeof
71+
72+**InvokeJSTypeOf**
73+
74+See the JOBResult_* constants in unit JOB_Shared.
75+
76+## Register a JS object/function
77+
78+By default the TJSObjectBridge registers only some basic JS objects/functions like *Object, Function, Date, String, Array, JSON, Promise, ArrayBuffer, Int8Array, Uint8Array*, etc.
79+
80+For example if you have the pas2js external class **TJSBird** for the JS object/function **Bird**, register it in JOB:
81+
82+``` pascal
83+type
84+ TJSBird = class external name 'Bird'
85+ ...
86+ end;
87+...
88+ if WADomBridge.RegisterGlobalObject(TJSBird,'Bird')=0 then
89+ raise Exception.Create('failed to register TJSBird');
90+```
91+
92+In the fpc/webassembly code create an interface:
93+
94+``` pascal
95+type
96+ IJSBird = interface
97+ [{guid}]
98+ end;
99+
100+ TJSBird = class(TJSObject,IJSBird)
101+ ...
102+ end;
103+
104+var JSBird: IJSBird; // this is the JS variable 'Bird'
105+
106+ JSBird := TJSBird.JOBCreateGlobal('Bird') as IJSBird;
107+```
108+
109+If the above Bird is a JS function you can create a **new** descendant with parameter 123 (in Javascript: **new Bird(123)**; ) by calling:
110+
111+``` pascal
112+var MyBird: IJSBird;
113+
114+ MyBird := JSBird.NewJSObject([123]) as IJSBird; // in JS: new Bird(123)
115+```
116+
117+For a full example see demo/wasienv/dom/WasiDomTest1.lpi and BrowserDomTest1.lpi
118+
119+# Create JOB units using webidl2pas
120+
121+Compile the latest and greatest webidl2pas utility from fpc main:
122+
123+utils/pas2js/webidl2pas.lpi
124+
125+Download some webidls for example from <https://hg.mozilla.org/mozilla-central/raw-file/tip/dom/webidl/>\<JSClassName\>
126+
127+Concatenate them into one file *Foo.webidl*.
128+
129+Create a text file *FooAlias.txt* containing the used classes from other units:
130+
131+ Object=IJSObject
132+ Set=IJSSet
133+ Map=IJSMap
134+ Function=IJSFunction
135+ Date=IJSDate
136+ RegExp=IJSRegExp
137+ String=IJSString
138+ Array=IJSArray
139+ ArrayBuffer=IJSArrayBuffer
140+ TypedArray=IJSTypedArray
141+ BufferSource=IJSBufferSource
142+ DataView=IJSDataView
143+ JSON=IJSJSON
144+ Error=IJSError
145+ TextEncoder=IJSTextEncode
146+ TextDecoder=IJSTextDecoder
147+
148+Create a text file *FooGlobals.txt* containing all the global JS variables in form Pascal variable name=JS class name,registered name:
149+
150+ JSFoo=Foo,foo
151+
152+Run the tool:
153+
154+`webidl2pas -f wasmjob -i Foo.webidl --typealiases=@FooAlias.txt --globals=@FooGlobals.txt`
155+
156+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.
157+
158+In the browser side you must register the global JS variables:
159+
160+``` pascal
161+ FWADomBridge.RegisterGlobalObject(foo,'foo');
162+```
163+
164+## Not yet supported webidl elements
165+
166+### Elements from other Pascal units
167+
168+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.
169+
170+### callback interfaces
171+
172+"callback interfaces" are a legacy definition. Remedy: Replace them manually with a callback
173+
174+### function returning a Dictionary
175+
176+ToDo: Define dictionary as class and interface and return an interface reference.
177+
178+### function returning a typed sequence
179+
180+At the moment it returns an untyped IJSArray.
181+
182+ToDo: returned a typed array.
183+
184+### function returning a callback
185+
186+### passing an array as argument
187+
188+### varargs
189+
190+Functions allowing to pass an arbitrary number of arguments.
191+
192+Workaround: User can call the InvokeX function directly.
193+
194+### constructor
195+
196+### getter
197+
198+### callback property
199+
200+A property with a callback, e.g. **onAbort**
201+
202+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.
203+
204+# JOB architecture
205+
206+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.
207+
208+## Data transfer between JS/WebAssembly
209+
210+JS/Webassembly interface only supports passing atomic types like boolean, integers and floats, not objects or strings.
211+
212+**Solution**:
213+
214+- Global objects like **document** and **window** are registered and queried by name.
215+- Every object is stored in an array with ID: TJOBObjectID
216+- ID is used to pass references to object between JS and Webassembly
217+- Lifetime is controlled from WebAssembly.
218+- By using interfaces, the lifetime of objects are controlled by the compiler.
219+- Methods can be called using an invoke mechanism.
220+- 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.
221+
222+## Function Arguments
223+
224+When calling a JS function from wasm, you can pass the following types/constants:
225+
226+- boolean
227+- integers (limited to double, because all numbers in JS are double, so up to 54 bits)
228+- double
229+- nil or Variants.Null
230+- string (utf8 converted to utf16, either use UTF8Encode/UTF8Decode or install a widestringmanager by using the units unicodeducet, unicodedata, fpwidestring)
231+- unicodestring
232+- widestring
233+- PChar - using strlen to get the size and utf8 converted to utf16
234+- PWideChar - using strlen to get the size
235+- TJSObject and IJSObject - its ObjectID is passed to the JS side, where the corresponding JS object is used
236+- Variant
237+- Variants.UnAssigned or JSUndefined
238+- TJOB_JSValue - at the moment needed for TJOB_Method.
239+
240+## Function Result
241+
242+Calling a JS function is done via the *InvokeJS*Result* functions, e.g. *aJSDate.InvokeJSUnicodeStringResult('toLocaleDateString',\[\])* which returns a *UnicodeString*.
243+
244+If the function does not exist, an *EJSInvoke* exception is raised.
245+
246+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.
247+
248+If the function returns an incompatible type, e.g. InvokeJSUnicodeStringResult returns a number, an *EJSInvoke* exception is raised.
249+
250+To retrieve any kind of JS value, use *InvokeJSVariantResult*.
251+
252+## Variants
253+
254+When an argument or result type does not have a simple type, webidl2pas uses **Variant**. Variants have a few newbie traps:
255+
256+For JS **undefined** you can use **Variants.Unassigned** and *VarIsEmpty(v)*.
257+
258+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.
259+
260+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.
261+
262+**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.
263+
264+## Callbacks
265+
266+An event handler in WebAssembly can be called from Javascript.
267+
268+At the moment only methods (of object) are supported.
269+
270+Every function type needs a callback, which decodes the arguments and encode the result.
271+
272+For example TJSEventHandler:
273+
274+``` pascal
275+type
276+ TJSEventHandler = function(Event: IJSEventListenerEvent): boolean of object;
277+...
278+function JOBCallTJSEventHandler(const aMethod: TMethod; var H: TJOBCallbackHelper): PByte;
279+var
280+ Event: IJSEventListenerEvent;
281+begin
282+ // get arguments. First as IJSEventListenerEvent
283+ Event:=H.GetObject(TJSEventListenerEvent) as IJSEventListenerEvent;
284+ // call the method and encode the result
285+ Result:=H.AllocBool(TJSEventHandler(aMethod)(Event));
286+end;
287+```
288+
289+TJOBCallbackHelper provides functions to decode arguments and encode the result.
290+
291+Passing a method as argument works like this:
292+
293+``` pascal
294+type
295+ IJSEventTarget = interface
296+ ['{1883145B-C826-47D1-9C63-47546BA536BD}']
297+ procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
298+ end;
299+
300+ TJSEventTarget = class(TJSObject,IJSEventTarget)
301+ procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
302+ end;
303+...
304+procedure TJSEventTarget.addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
305+var
306+ m: TJOB_JSValueMethod;
307+begin
308+ // combine the users method and the callback into one argument m
309+ m:=TJOB_JSValueMethod.Create(TMethod(aListener),@JOBCallTJSEventHandler);
310+ try
311+ // call the JS function addEventListener(aName,m)
312+ InvokeJSNoResult('addEventListener',[aName,m]);
313+ finally
314+ m.Free;
315+ end;
316+end;
317+```
318+
319+See units job_js and job_web for more examples.
320+
321+# Implementation details
322+
323+Here are some technical notes describing the various architectural decisions.
324+
325+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](https://github.com/mozilla/gecko-dev/tree/master/dom/webidl)
326+
327+``` pascal
328+IJSElement = interface(IJSObject)
329+ ['someawfulGUID']
330+ function childElementCount : Integer;
331+ function firstElementChild : IJSElement;
332+ // all other
333+end;
334+```
335+
336+In implementation, the following kind of code can be found:
337+
338+``` pascal
339+// Hand crafted in e.g. JSObject unit
340+ TJSObject = class(TInterfacedObject,IJSObject)
341+ public
342+ constructor JOBCast(Intf: IJSObject); overload;
343+ constructor JOBCreateFromID(aID: TJOBObjectID); virtual; // use this only for the owner (it will release it on free)
344+ constructor JOBCreateGlobal(const aID: UnicodeString); virtual;
345+ class function Cast(Intf: IJSObject): IJSObject; overload;
346+ destructor Destroy; override;
347+ property JOBObjectID: TJOBObjectID read FJOBObjectID;
348+ property JOBObjectIDOwner: boolean read FJOBObjectIDOwner write FJOBObjectIDOwner;
349+ property JOBCastSrc: IJSObject read FJOBCastSrc; // nil means it is the original, otherwise it is a typecast
350+ property ObjectID: TJOBObjectID read FObjectID;
351+ // call a function
352+ procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall); virtual;
353+ function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Boolean; virtual;
354+ function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Double; virtual;
355+ function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): UnicodeString; virtual;
356+ function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeType = jiCall): TJSObject; virtual;
357+ function InvokeJSVariantResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Variant; virtual;
358+ function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): String; virtual;
359+ function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): LongInt; virtual;
360+ function InvokeJSMaxIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): int64; virtual;
361+ function InvokeJSTypeOf(const aName: string; Const Args: Array of const): TJOBResult; virtual;
362+ // read a property
363+ function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
364+ function ReadJSPropertyDouble(const aName: string): double; virtual;
365+ function ReadJSPropertyUnicodeString(const aName: string): UnicodeString; virtual;
366+ function ReadJSPropertyObject(const aName: string; aResultClass: TJSObjectClass): TJSObject; virtual;
367+ function ReadJSPropertyUtf8String(const aName: string): string; virtual;
368+ function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
369+ function ReadJSPropertyInt64(const aName: string): Int64; virtual;
370+ function ReadJSPropertyVariant(const aName: string): Variant; virtual;
371+ // write a property
372+ procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
373+ procedure WriteJSPropertyDouble(const aName: string; Value: Double); virtual;
374+ procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
375+ procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
376+ procedure WriteJSPropertyObject(const aName: string; Value: IJSObject); virtual;
377+ procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
378+ procedure WriteJSPropertyInt64(const aName: string; Value: Int64); virtual;
379+ procedure WriteJSPropertyVariant(const aName: string; const Value: Variant); virtual;
380+ // create a new object using the new-operator
381+ function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
382+ end;
383+```
384+
385+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.
386+
387+That function does the actual call: it uses **ObjectID** to look for the *Self* object in an array:
388+
389+- Negative IDs are special: window, document.
390+- positive IDs are temporary objects created via the **InvokeJSObjectResult**, see below.
391+
392+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.
393+
394+If the result is an object, an ID is generated (simple counter), the result value is stored in an array **FLocalObjects**.
395+
396+The ID is returned to the webassembly, which will use the ID to create a *TJSObject* descendent.
397+
398+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.
399+
400+The above is a basic invoke mechanism for Javascript code.
401+
402+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:
403+
404+- The interface (see above for an example)
405+- An implementation object as below, descendant of **TJSObject**
406+
407+``` pascal
408+// Generated from webIDL in jsweb/jsdom unit.
409+
410+IJSElement = interface(IJSNode)
411+ function childElementCount : Integer;
412+ function firstElementChild : IJSElement;
413+end;
414+
415+TJSElementImpl = class(TJSObject,IJSElement)
416+ function childElementCount : Integer;
417+ function firstElementChild : IJSElement;
418+ // all other
419+end;
420+
421+function TJSElementImpl.childElementCount : Integer;
422+begin
423+ Result:=ReadJSPropertyLongInt('childElementCount');
424+end;
425+
426+function TJSElementImpl.firstElementChild : IJSElement;
427+begin
428+ Result:=ReadJSPropertyObject('firstElementChild',TJSElementImpl) as IJSElement;
429+end;
430+```
431+
432+# ToDos
433+
434+- read/write array elements
435+- store/cache callbacks to support removeEventListener
436+- move job_web+job_js units to fpc
437+- webidl2pas:
438+ - stringifier
439+ - check Getter+Setter name conflicts
440+ - argument of sequence\<something\>
441+ - overloaded version for dictionary args
442+ - varargs
443+ - js getter
444+ - js constructor
445+- pas2jsdsgn: new wasmjob browser project
446+
447+---
448+*From the Free Pascal wiki, page "WebAssembly/DOM", as it stood on 2024-04-12 after a change by Mattias2, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
449+Category: WebAssembly

Back to history