WebAssembly/Internals

From Free Pascal wiki
Revision as of 18:41, 22 September 2019 by Skalogryz (talk | contribs) (→‎See Also)
Jump to navigationJump to search

Limitations

  • WebAssembly doesn't have a stand-alone executable format. It's always a "plugin" to some environment. Thus the compiler is currently only supports producing of libraries and program should not be used. Using library requires exporting a certain functions from the library. Those functions will be "export" part of Wasm module as well.
  • WebAssembly format doesn't provide any special information for static linking (for the purpose of combining multiple .wasm files into one). No support is provided at this time, thus entire pascal code should fit a single library file.
  • Importing of functions into pascal is supported partially. see External
  • Memory object must always be provided externally, by the module env with name memory. This is hard-coded in Wasm assembler. (todo: needs to be configurable)

Validation

There's a certain requirements to the binary code of WebAssembly.

The validation is described at the specification http://webassembly.github.io/spec/core/valid/index.html

A code that satisfies the validation rules, is considered to be well-formed


It's possible to generate a binary file with the code that doesn't satisfy the validation steps. However, the browsers would not instantiate such binary file. Neither any other host should.

In some cases, there might be an overhead to create well-formed code.

Global Symbols

Global symbols are stored in global variables. Each variable is of i32 type and stores an offset in memory, where the object is usually stored.

In order to get the address of the variable, one should invoke get_global for the desired symbols. The memory offset if predefined and cannot be changed during the runtime. The contents of the memory can be changed.

Code Branching/Flow Control

todo: rewrite to the better explanation

WebAssembly doesn't allow direct jumps to (address/label)/or jump by offset. It only allows to jump "out-of" a code block, where the code block would be identified by a label. I.e. beginning of the loop label (for continue) or end of the loop label (for break). Loop itself is a single code block, known as "loop" in WebAssembly ).

FPC basic implementation is based of the ability of conditional jump instructions to a specified label. For that reason, the default implementation of TCGIfNode doesn't apply. Also, not every conditional jump instructions can have a label assign to it. Only instructions that jump out of the block are should use labels. Other, simply rely on the fact, that the next instruction IS the point they needs to be.

IF-block

Wasm IF do return a value (one of the basic WebAsm types). Pascal IF-statement does not return a value.

Thus the feature of value returning is not used (todo: but should be).

A dummy 0 value is pushed on the stack. IF is hard-coded to return i32 at all times.


According to the official documentation, IF always comes with "ELSE" block. However, some earlier versions do allow IF without an else statement. (which is also allowed in Binary specification of WASM). Such IF comes without any resulting type, and the execution of IF should leave stack unmodified in the end.

At this time "ELSE" branch is always generated for any IF statement. However, if there's no actual ELSE code (in pascal), a single "nop" instruction and a dummy result value produced.

function cmp(a,b: integer): Integer; 
begin
  if a > b then 
    cmp := 1
  else 
    cmp := 0;
end;

turns into:

(func $cmp
	(param	i32)
	(param	i32)
	(result	i32)
	(local	i32)	;; Temp 2,1 allocated
	(local	i32)	;; Temp 3,1 allocated
;; [3] begin
;; [4] if a > b then
	get_local	0
	set_local	3
	get_local	3
	get_local	1
	i32.gt_s
	if (result i32)  ;; hard-coded i32 type of IF result
;; [5] cmp := 1
	i32.const	1
	set_local	2
	i32.const	0 ;; mandatory wasm-IF result 
	else
;; [7] cmp := 0;
	i32.const	0
	set_local	2
	i32.const	0  ;; mandatory wasm-IF result
	end
	drop            ;; dropping IF-result from the stack, as unused
;; [8] end;
	get_local	2
	return
)

The code generation of cifnode is overridden.

Loop-block

Loop is a block of instructions that has the label set to the begging of the code.

Here's an example if Loop. The function tries to sum-app a number X amount of times

function mulbyadd(Num, Cnt: integer): integer;
  Result := 0;
  while Cnt>0 do begin
    dec(Cnt);
    Result := Result + Num;
  end;
end;
  (func $mulbyadd (param $Num i32) (param $Cnt i32) (result i32)
    (local i32)       ;; local variable (that gets index #2) this is $Result
    i32.const 0
    set_local 2

    block
    loop 
      get_local $Cnt  ;; comparing Cnt to 0
      i32.const 0     ;; 
      i32.le_s         
      br_if 1         ;; if Cnt is less or equal to 0, then we should exit the loop

      get_local $Cnt  ;; decreasing Cnt
      i32.const 1 
      i32.sub  
      set_local $Cnt  

      get_local $Num  ;;
      get_local 2     ;; 
      i32.add         ;;
      set_local 2     ;; Result := Result + Num

      br 0            ;; loop back to the beginning 
    end 
    end
    get_local 2       ;; pushing the result to the stack (for the return value)
    )

FPC Labels

Since branching is relative in Wasm (the absolute goto-like is only a proposed feature for Wasm), the use of TAsmLabel is different than other targets. The field labelnr is changed to indicate the relative jump. Typically 0 - to return to the loop or 1 - to jump out of the loop - or N to exit the function.

External

One can declare an external function. For a WebAssembly module the function must be provided during a module instantiation. Any external function must have a library name specified (Static linking is not supported!). The library name and the "exteral" name will be used during instantiation phase.

Only basic types can be used as parameters (integer, floats, memory pointers) and function result.

Here's an example:

The code tries to use an external function that accepts only 1 integer parameter. (The function should write out the parameter)

library testext;

procedure logint(i: integer); external 'env' name 'logint'; // name of external module and function name

procedure run;
begin
  logint(304);
end;

exports
  run name 'run';
end.


In the browser, during instantiation the function must be provided:

const importObject  = {
  // note "env" should match the name of the function's library used in the pascal 
  // thus it's possible to import functions from different sources.
  "env":  {

    "memory": new WebAssembly.Memory({initial:32, maximum:32})
    // this is the implementation of "logint" function
    // it accepts exactly 1 parameter, and puts it into a the browser's console
   ,"logint": function(k) { console.log(k) }
  }
    
}
fetch('../out/main.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => WebAssembly.instantiate(bytes, importObject)).then(results => {
  instance = results.instance;
 
  document.getElementById("container").textContent = 
    // running the export run() function
    instance.exports.run();
}).catch(console.error);

PChar Strings

Strings would require a more work from the client side. There's no "string" type in Wasm, thus strings are treated as chunks in memory. From Javascript perspective that means, that a reference to a memory is received from a Wasm code and should be converted to JS string (that typically involves a text decoder)

library testext;

type
  pchar = ^char;

procedure logstr(p: pchar); external 'env' name 'logstr';
procedure writedom(p: pchar); external 'dom' name 'writedom';

procedure run;
begin
  logstr('hello');
  logstr('world');
  writedom('<b>Hello World</b> and <input type="button" value="FPC">');
end;

exports
  run name 'run';

end.

On the host side the function needs to read the memory and prepare the string for the output:

const memory = new WebAssembly.Memory({initial:32, maximum:32});

// the function takes, strofs that indicates the start of string
// and then tries to get the string out of the memory, by searching for null-terminating character
function getStr(strofs)
{
  var arr = new Uint8Array(memory.buffer,strofs);
  for (var i=0; i < arr.length; i++) 
    if (arr[i]==0) // null terminating char is found
      arr = arr.subarray(0, i);
  var str = new TextDecoder("utf-8").decode(arr);
  return str;
}


const importObject  = {
  "env":  {
    "memory": memory
   ,"logstr": function(k) 
     { console.log(getStr(k));  }
  },
  "dom": {
   "writedom": function(k) {  document.write(getStr(k)) }
  }
}
fetch('../out/main.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => WebAssembly.instantiate(bytes, importObject)).then(results => {
  instance = results.instance;
  document.getElementById("container").textContent = instance.exports.run();
}).catch(console.error);

Obviously pascal Ansi or WideStrings can work in the similar manner, and can be more efficient (as the size of the string is known ahead of time). However, strings can only be passed as constant strings, rather than a string variable for change (as implementing a reference count between Wasm and JS can be a bit more complicated)

Static Linking

See LLD linker: https://lld.llvm.org/WebAssembly.html

See Also