Sign in Join
Wiki copytemporary

WebAssembly/DOM: what changed

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

OldNewLine
11Accessing JS Objects from [[WebAssembly/Compiler|WebAssembly]].
22
33# What is JOB?
44
55JS Object Bridge - JOB
66
77JOB provides:
88
99- Units to communicate between fpc wasm and pas2js browser to call JS functions, get and set JS properties and set callbacks.
1010- Units for fpc wasm with common browser classes.
1111- A tool webidl2pas to help generating new units for JS classes from webidls.
1212
1313# Using JOB
1414
1515A JOB program has a webassembly program (fpc wasi) and a browser (pas2js) program.
1616
1717The browser side contains the html and registers all global JS variables needed by the webassembly side.
1818
1919See the pas2js demo/wasienv/button/BrowserButton1.lpi
2020
2121<https://gitlab.com/freepascal.org/fpc/pas2js/-/tree/main/demo/wasienv/button>
2222
2323## JS Classes
2424
2525Each JS class like **HTMLButtonElement** have a FPC class (*TJSHTMLButtonElement*) with a reference counted interface (*IJSHTMLButtonElement*).
2626
2727Normally functions return an interface and expect interfaces as arguments.
2828
2929## Callbacks
3030
3131An event handler in WebAssembly can be called from Javascript.
3232
3333At the moment only methods (of object) are supported.
3434
3535``` pascal
3636 TWasmApp = class
3737 ...
3838 function OnButtonClick(Event: IJSEvent): boolean;
3939 ...
4040 end;
4141
4242function TWasmApp.OnButtonClick(Event: IJSEvent): boolean;
4343begin
4444 JSWindow.Alert('You triggered TWasmApp.OnButtonClick');
4545 Result:=true;
4646end;
4747...
4848 JSButton.addEventListener('click',@OnButtonClick);
4949...
5050```
5151
5252## Type casts
5353
5454Often 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:
5555
5656``` pascal
5757var
5858 Elem: IJSElement;
5959 HTMLElem: IJSHTMLElement;
6060begin
6161 Elem := JSDocument.getElementById('button');
6262 // type casting to IJSHTMLElement requires creating a new bridge object using TJSHTMLElement.Cast:
6363 HTMLElem := TJSHTMLElement.Cast(Elem);
6464 // Note: since Elem and HTMLElem are reference counted interfaces, the compiler automatically frees temporary objects.
6565end;
6666```
6767
6868This **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,
6969
7070## Typeof
7171
7272**InvokeJSTypeOf**
7373
7474See the JOBResult_* constants in unit JOB_Shared.
7575
7676## Register a JS object/function
7777
7878By default the TJSObjectBridge registers only some basic JS objects/functions like *Object, Function, Date, String, Array, JSON, Promise, ArrayBuffer, Int8Array, Uint8Array*, etc.
7979
8080For example if you have the pas2js external class **TJSBird** for the JS object/function **Bird**, register it in JOB:
8181
8282``` pascal
8383type
8484 TJSBird = class external name 'Bird'
8585 ...
8686 end;
8787...
8888 if WADomBridge.RegisterGlobalObject(TJSBird,'Bird')=0 then
8989 raise Exception.Create('failed to register TJSBird');
9090```
9191
9292In the fpc/webassembly code create an interface:
9393
9494``` pascal
9595type
9696 IJSBird = interface
9797 [{guid}]
9898 end;
9999
100100 TJSBird = class(TJSObject,IJSBird)
101101 ...
102102 end;
103103
104104var JSBird: IJSBird; // this is the JS variable 'Bird'
105105
106106 JSBird := TJSBird.JOBCreateGlobal('Bird') as IJSBird;
107107```
108108
109109If the above Bird is a JS function you can create a **new** descendant with parameter 123 (in Javascript: **new Bird(123)**; ) by calling:
110110
111111``` pascal
112112var MyBird: IJSBird;
113113
114114 MyBird := JSBird.NewJSObject([123]) as IJSBird; // in JS: new Bird(123)
115115```
116116
117117For a full example see demo/wasienv/dom/WasiDomTest1.lpi and BrowserDomTest1.lpi
118118
119119# Create JOB units using webidl2pas
120120
121121Compile the latest and greatest webidl2pas utility from fpc main:
122122
123123utils/pas2js/webidl2pas.lpi
124124
125125Download some webidls for example from <https://hg.mozilla.org/mozilla-central/raw-file/tip/dom/webidl/>\<JSClassName\>
126126
127127Concatenate them into one file *Foo.webidl*.
128128
129129Create a text file *FooAlias.txt* containing the used classes from other units:
130130
131131 Object=IJSObject
132132 Set=IJSSet
133133 Map=IJSMap
134134 Function=IJSFunction
135135 Date=IJSDate
136136 RegExp=IJSRegExp
137137 String=IJSString
138138 Array=IJSArray
139139 ArrayBuffer=IJSArrayBuffer
140140 TypedArray=IJSTypedArray
141141 BufferSource=IJSBufferSource
142142 DataView=IJSDataView
143143 JSON=IJSJSON
144144 Error=IJSError
145145 TextEncoder=IJSTextEncode
146146 TextDecoder=IJSTextDecoder
147147
148148Create a text file *FooGlobals.txt* containing all the global JS variables in form Pascal variable name=JS class name,registered name:
149149
150150 JSFoo=Foo,foo
151151
152152Run the tool:
153153
154-`webidl2pas -f wasmjob -i Foo.webidl --typealiases=@FooAlias.txt --globals=@FooGlobals.txt`
154+```text
155+webidl2pas -f wasmjob -i Foo.webidl --typealiases=@FooAlias.txt --globals=@FooGlobals.txt
156+```
155157
156158If 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.
157159
158160In the browser side you must register the global JS variables:
159161
160162``` pascal
161163 FWADomBridge.RegisterGlobalObject(foo,'foo');
162164```
163165
164166## Not yet supported webidl elements
165167
166168### Elements from other Pascal units
167169
168170At 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.
169171
170172### callback interfaces
171173
172174"callback interfaces" are a legacy definition. Remedy: Replace them manually with a callback
173175
174176### function returning a Dictionary
175177
176178ToDo: Define dictionary as class and interface and return an interface reference.
177179
178180### function returning a typed sequence
179181
180182At the moment it returns an untyped IJSArray.
181183
182184ToDo: returned a typed array.
183185
184186### function returning a callback
185187
186188### passing an array as argument
187189
188190### varargs
189191
190192Functions allowing to pass an arbitrary number of arguments.
191193
192194Workaround: User can call the InvokeX function directly.
193195
194196### constructor
195197
196198### getter
197199
198200### callback property
199201
200202A property with a callback, e.g. **onAbort**
201203
202204At 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.
203205
204206# JOB architecture
205207
206208Pascal 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.
207209
208210## Data transfer between JS/WebAssembly
209211
210212JS/Webassembly interface only supports passing atomic types like boolean, integers and floats, not objects or strings.
211213
212214**Solution**:
213215
214216- Global objects like **document** and **window** are registered and queried by name.
215217- Every object is stored in an array with ID: TJOBObjectID
216218- ID is used to pass references to object between JS and Webassembly
217219- Lifetime is controlled from WebAssembly.
218220- By using interfaces, the lifetime of objects are controlled by the compiler.
219221- Methods can be called using an invoke mechanism.
220222- 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.
221223
222224## Function Arguments
223225
224226When calling a JS function from wasm, you can pass the following types/constants:
225227
226228- boolean
227229- integers (limited to double, because all numbers in JS are double, so up to 54 bits)
228230- double
229231- nil or Variants.Null
230232- string (utf8 converted to utf16, either use UTF8Encode/UTF8Decode or install a widestringmanager by using the units unicodeducet, unicodedata, fpwidestring)
231233- unicodestring
232234- widestring
233235- PChar - using strlen to get the size and utf8 converted to utf16
234236- PWideChar - using strlen to get the size
235237- TJSObject and IJSObject - its ObjectID is passed to the JS side, where the corresponding JS object is used
236238- Variant
237239- Variants.UnAssigned or JSUndefined
238240- TJOB_JSValue - at the moment needed for TJOB_Method.
239241
240242## Function Result
241243
242244Calling a JS function is done via the *InvokeJS*Result* functions, e.g. *aJSDate.InvokeJSUnicodeStringResult('toLocaleDateString',\[\])* which returns a *UnicodeString*.
243245
244246If the function does not exist, an *EJSInvoke* exception is raised.
245247
246248If 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.
247249
248250If the function returns an incompatible type, e.g. InvokeJSUnicodeStringResult returns a number, an *EJSInvoke* exception is raised.
249251
250252To retrieve any kind of JS value, use *InvokeJSVariantResult*.
251253
252254## Variants
253255
254256When an argument or result type does not have a simple type, webidl2pas uses **Variant**. Variants have a few newbie traps:
255257
256258For JS **undefined** you can use **Variants.Unassigned** and *VarIsEmpty(v)*.
257259
258260For 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.
259261
260262Variants 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.
261263
262264**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.
263265
264266## Callbacks
265267
266268An event handler in WebAssembly can be called from Javascript.
267269
268270At the moment only methods (of object) are supported.
269271
270272Every function type needs a callback, which decodes the arguments and encode the result.
271273
272274For example TJSEventHandler:
273275
274276``` pascal
275277type
276278 TJSEventHandler = function(Event: IJSEventListenerEvent): boolean of object;
277279...
278280function JOBCallTJSEventHandler(const aMethod: TMethod; var H: TJOBCallbackHelper): PByte;
279281var
280282 Event: IJSEventListenerEvent;
281283begin
282284 // get arguments. First as IJSEventListenerEvent
283285 Event:=H.GetObject(TJSEventListenerEvent) as IJSEventListenerEvent;
284286 // call the method and encode the result
285287 Result:=H.AllocBool(TJSEventHandler(aMethod)(Event));
286288end;
287289```
288290
289291TJOBCallbackHelper provides functions to decode arguments and encode the result.
290292
291293Passing a method as argument works like this:
292294
293295``` pascal
294296type
295297 IJSEventTarget = interface
296298 ['{1883145B-C826-47D1-9C63-47546BA536BD}']
297299 procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
298300 end;
299301
300302 TJSEventTarget = class(TJSObject,IJSEventTarget)
301303 procedure addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
302304 end;
303305...
304306procedure TJSEventTarget.addEventListener(const aName: UnicodeString; const aListener: TJSEventHandler);
305307var
306308 m: TJOB_JSValueMethod;
307309begin
308310 // combine the users method and the callback into one argument m
309311 m:=TJOB_JSValueMethod.Create(TMethod(aListener),@JOBCallTJSEventHandler);
310312 try
311313 // call the JS function addEventListener(aName,m)
312314 InvokeJSNoResult('addEventListener',[aName,m]);
313315 finally
314316 m.Free;
315317 end;
316318end;
317319```
318320
319321See units job_js and job_web for more examples.
320322
321323# Implementation details
322324
323325Here are some technical notes describing the various architectural decisions.
324326
325327The 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)
326328
327329``` pascal
328330IJSElement = interface(IJSObject)
329331 ['someawfulGUID']
330332 function childElementCount : Integer;
331333 function firstElementChild : IJSElement;
332334 // all other
333335end;
334336```
335337
336338In implementation, the following kind of code can be found:
337339
338340``` pascal
339341// Hand crafted in e.g. JSObject unit
340342 TJSObject = class(TInterfacedObject,IJSObject)
341343 public
342344 constructor JOBCast(Intf: IJSObject); overload;
343345 constructor JOBCreateFromID(aID: TJOBObjectID); virtual; // use this only for the owner (it will release it on free)
344346 constructor JOBCreateGlobal(const aID: UnicodeString); virtual;
345347 class function Cast(Intf: IJSObject): IJSObject; overload;
346348 destructor Destroy; override;
347349 property JOBObjectID: TJOBObjectID read FJOBObjectID;
348350 property JOBObjectIDOwner: boolean read FJOBObjectIDOwner write FJOBObjectIDOwner;
349351 property JOBCastSrc: IJSObject read FJOBCastSrc; // nil means it is the original, otherwise it is a typecast
350352 property ObjectID: TJOBObjectID read FObjectID;
351353 // call a function
352354 procedure InvokeJSNoResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall); virtual;
353355 function InvokeJSBooleanResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Boolean; virtual;
354356 function InvokeJSDoubleResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Double; virtual;
355357 function InvokeJSUnicodeStringResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): UnicodeString; virtual;
356358 function InvokeJSObjectResult(const aName: string; Const Args: Array of const; aResultClass: TJSObjectClass; Invoke: TJOBInvokeType = jiCall): TJSObject; virtual;
357359 function InvokeJSVariantResult(const aName: string; Const Args: Array of const; Invoke: TJOBInvokeType = jiCall): Variant; virtual;
358360 function InvokeJSUtf8StringResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): String; virtual;
359361 function InvokeJSLongIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): LongInt; virtual;
360362 function InvokeJSMaxIntResult(const aName: string; Const args: Array of const; Invoke: TJOBInvokeType = jiCall): int64; virtual;
361363 function InvokeJSTypeOf(const aName: string; Const Args: Array of const): TJOBResult; virtual;
362364 // read a property
363365 function ReadJSPropertyBoolean(const aName: string): boolean; virtual;
364366 function ReadJSPropertyDouble(const aName: string): double; virtual;
365367 function ReadJSPropertyUnicodeString(const aName: string): UnicodeString; virtual;
366368 function ReadJSPropertyObject(const aName: string; aResultClass: TJSObjectClass): TJSObject; virtual;
367369 function ReadJSPropertyUtf8String(const aName: string): string; virtual;
368370 function ReadJSPropertyLongInt(const aName: string): LongInt; virtual;
369371 function ReadJSPropertyInt64(const aName: string): Int64; virtual;
370372 function ReadJSPropertyVariant(const aName: string): Variant; virtual;
371373 // write a property
372374 procedure WriteJSPropertyBoolean(const aName: string; Value: Boolean); virtual;
373375 procedure WriteJSPropertyDouble(const aName: string; Value: Double); virtual;
374376 procedure WriteJSPropertyUnicodeString(const aName: string; const Value: UnicodeString); virtual;
375377 procedure WriteJSPropertyUtf8String(const aName: string; const Value: String); virtual;
376378 procedure WriteJSPropertyObject(const aName: string; Value: IJSObject); virtual;
377379 procedure WriteJSPropertyLongInt(const aName: string; Value: LongInt); virtual;
378380 procedure WriteJSPropertyInt64(const aName: string; Value: Int64); virtual;
379381 procedure WriteJSPropertyVariant(const aName: string; const Value: Variant); virtual;
380382 // create a new object using the new-operator
381383 function NewJSObject(Const Args: Array of const; aResultClass: TJSObjectClass): TJSObject; virtual;
382384 end;
383385```
384386
385387The 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.
386388
387389That function does the actual call: it uses **ObjectID** to look for the *Self* object in an array:
388390
389391- Negative IDs are special: window, document.
390392- positive IDs are temporary objects created via the **InvokeJSObjectResult**, see below.
391393
392394The **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.
393395
394396If the result is an object, an ID is generated (simple counter), the result value is stored in an array **FLocalObjects**.
395397
396398The ID is returned to the webassembly, which will use the ID to create a *TJSObject* descendent.
397399
398400The 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.
399401
400402The above is a basic invoke mechanism for Javascript code.
401403
402404This 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:
403405
404406- The interface (see above for an example)
405407- An implementation object as below, descendant of **TJSObject**
406408
407409``` pascal
408410// Generated from webIDL in jsweb/jsdom unit.
409411
410412IJSElement = interface(IJSNode)
411413 function childElementCount : Integer;
412414 function firstElementChild : IJSElement;
413415end;
414416
415417TJSElementImpl = class(TJSObject,IJSElement)
416418 function childElementCount : Integer;
417419 function firstElementChild : IJSElement;
418420 // all other
419421end;
420422
421423function TJSElementImpl.childElementCount : Integer;
422424begin
423425 Result:=ReadJSPropertyLongInt('childElementCount');
424426end;
425427
426428function TJSElementImpl.firstElementChild : IJSElement;
427429begin
428430 Result:=ReadJSPropertyObject('firstElementChild',TJSElementImpl) as IJSElement;
429431end;
430432```
431433
432434# ToDos
433435
434436- read/write array elements
435437- store/cache callbacks to support removeEventListener
436438- move job_web+job_js units to fpc
437439- webidl2pas:
438440 - stringifier
439441 - check Getter+Setter name conflicts
440442 - argument of sequence\<something\>
441443 - overloaded version for dictionary args
442444 - varargs
443445 - js getter
444446 - js constructor
445447- pas2jsdsgn: new wasmjob browser project
446448
447449---
448450*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).*
449451Category: WebAssembly

Back to history