wat

참고
[1] https://developer.mozilla.org/ko/docs/WebAssembly/Understanding_the_text_format
[2] https://developer.mozilla.org/ko/docs/WebAssembly/Text_format_to_wasm


S-expressions

  • wat의 기본적인 텍스트 구조는 S-expressions이다.
  • S-expressions는 트리를 텍스트 형식으로 방법이다.
  • Abstract Tree와는 다르게 단순하면서 일반적으로 많이 사용하는 방식으로 구성되어 있다.
  • 트리의 각 노드는 괄호를 통해 나타낸다.
  • 아래 wat 코드는 모듈이라는 최상위 노드와 2개의 자식 노드를 가진 트리이다.
    (module (memory 1) (func))
    
  • wat의 모든 코드는 다음과 같은 구조를 갖는 함수로 구성되어있다.
    (func <signature> <locals> <body>)
    

Signatures and parameters

  • 함수의 parameter와 return의 타입을 지정한다. (param result 순으로 배치한다)
  • 현재는 단 하나의 반환 타입을 가질 수 있다.
  • result 노드가 없다면 아무것도 반환하지 않는다.
  • i32, i64, f32, f64 타입만 가능하다.
  • 아래의 코드는 2개의 32bit int를 받고 64bit float를 반환하는 함수이다.
    (func (param i32) (param i32) (result f64) ...)
    
  • 함수 내부에서 사용할 local 변수를 설정할 수도 있다.
  • signatures 뒤에 (local type)을 통해서 선언한다.
    (func (param i32) (param i32) (result f64) (local i32) ...)
    

local와 parameter를 getting, setting 하기

  • get_local 명령어와 set_local 명령어로 함수의 parameter와 local 변수를 읽고 쓸 수 있다.
    (func (param i32) (param f32) (local f64)
    get_local 0  // param i32 매개변수를 받는다.
    get_local 1  // param f32 매개변수를 받는다.
    get_local 2) // local f64 매개변수를 받는다.
    
  • 위의 코드처럼 index를 통해서 변수를 받지 않고, 이름을 통해서도 받을 수 있다.
    (func (param $p1 i32) (param $p2 f32) (local $loc f64)
    get_local $p1
    get_local $p2
    get_local $loc)
    

stack machines

  • 앞서 본 코드에서 보면 함수의 지역변수를 받아올 때 따로 저장하는 레지스터를 등록하지 않았다. 이것은 webassembly가 기본적으로 stack machine이기 때문이다.
  • 모든 데이터를 스택에 넣고 빼는 방식으로 진행된다. get_local 명령어를 통해 스택에 넣는다.
  • 스택은 함수마다 하나의 스택을 사용한다고 생각해야한다.
  • 리턴 데이터가 하나이면 스택에 하나의 데이터가 존재해야하고 리턴하지 않으면 빈스택으로 끝 맞춰야 한다.
    (func (param $p1 i32) (param $p2 i32) (result i32) // result i32 이므로 마지막에 스택에는 i32형 데이터 하나가 남아있어야 한다.
    get_local $p1
    get_local $p2
    i32.add)
    

함수 호출하기

  • 변수와 마찬기지로 함수 또한 인덱스를 통해서 식별되지만 이름을 붙일 수도 있다.
    (func $add ...)
    
  • 함수를 외부에서 사용하기 위해서는 해당 함수를 export해야만 한다.
    (module
    (func $add (param $p1 i32) (param $p2 i32) (result i32)
      get_local $p1
      get_local $p2
      i32.add)
    (export "add" (func $add)) // add라는 이름으로 add 함수를 export한다.
    )
    
  • 위의 코드를 자바스크립트 코드에서 사용하기 위해서는 instance.exports.function을 하면된다.
    WebAssembly.instantiateStreaming(fetch('add.wasm'))
    .then(resolve => {
      result = resolve.instance.exports.add(1, 2); // result = 3
    });
    

같은 wat 코드 내에서 다른 함수 호출하기

  • wat의 call 명령어는 모듈 내에서 단일 함수를 호출한다.
    (module 
      (func $getAnswer) (result i32)
          i32.const 42) // stack에 42를 쌓는건가??
      (func (export "getAnswerPlus1") (result i32) // function의 정의와 함께 export 함수임을 알릴 수 있다. 요게 편한듯...
          call $getAnswer // 함수 getAnswer 호출, stack에 42가 쌓임
          i32.const 1 // stack에 1이 쌓임
          i32.add)) // 42 와 1 
    

자바스크립트에서 함수 가져오기

  • wat 모듈에서 자바스크립트 함수를 불러올 수도 있다… 어떻게 가능한건지는 모르겟지만… 그 함수인 부분은 자바스크립트 코드를 실행시킬듯…
  • 아래 코드에서 “console” “log” 은 두단계의 네임스페이스를 의미하여 console 모듈의 log 함수를 가져 오기를 요청한다.
  • 가져온 함수를 (func …)을 통해 선언해줘야 한다.
    (module
      (import "console" "log" (func $log (param i32)))
      (func (export "logIt")
          i32.const 13 // stack에 13 push
          call $log)) // stack에 있는 13 입력으로
    
  • 자바스크립트에서는 wat 모듈로 함수를 보낼려면 instantiate 함수에 importObject를 인수로 넣어야 한다.
    const importObject = {
      console: {
          log: function(arg){ // int32 만 가능
              console.log(arg);
          }
      }
    };
    WebAssembly.instantiateStreaming(fetch('logger.wasm'), importObject)
    .then(obj => {
      obj.instance.exports.logIt();
    });
    

wat 모듈에서의 전역 변수

  • wasm은 하나 이상의 모듈에서 사용가능하고 자바스크립트에서 읽기와 쓰기가 가능한 전역 변수를 선언할 수 있다.
  • 자바스크립트에서는 WebAssembly.Global() 생성자를 사용하여 접근해야한다.
  • 자바스크립트에서 접근하기 위해서는 반드시 import를 해야한다.
    (module
      (global $g (import "js" "global") (mut i32)) // 자바스크립트에서 wasm의 전역변수를 사용하고 싶다면 import를 해줘야한다.
      (func (export "getGlobal") (result i32)
          (get_global $g)) // g global 변수를 얻어서 스택에 저장.
      (func (export "incGlobal")
          (set_global $g
              (i32.add (get_global $g) (i32.const 1)))) // add 함수가 그냥 쌩으로 있으면 스택에서 가져와서 쓰지만 이렇게도 쓸 수 있다. 즉석으로 스택에 넣고 빼기??
    
    const global = new WebAssembly.Global({value:'i32', mutable:true}, 0);
    WebAssembly.instantiateStreaming(fetch('global.wasm'), { js: { global }})
    .then(result => {
      let v = result.instace.exports.getGlobal(); // value = 0
      global.value = 42;
      v = result.instance.exports.getGlobal(); // value = 42
      result.instace.exports.incGlobal(); // wasm 에서 export하는 함수로 global의 value 값을 1 증가시켜준다.
      v = result.instance.exports.getGlobal(); // value = 43
    })
    

wat 모듈에서의 momory

  • wasm에는 linear memory 에서 여러 데이터를 한번에 읽고 쓰는 데 필요한 i32.load 및 i32.store 를 제공한다.
  • 자바스크립트 WebAssembly.Memory 생성자를 통해서 자바스크립트와 wasm 코드간에 메모리 접근을 공유한다.
  • 자바스크립트에서 생성한 메모리를 wasm에서 사용하기 위해서는 import를 해야한다.
  • 할당된 메모리는 data section에 지정되며, 그래서 data section에 데이터를 저장하면 된다.
    (module
      (import "console" "log" (func $log (param i32 i32))) // 이런식으로 (param i32) (param i32) 를 (param i32 i32)으로 써도 되는듯.
      (import "js" "mem" (memory 1)) // 메모리를 import 하는데 메모리의 최소 크기는 1페이지이다. 라는뜻..
      (data (i32.const 0) "Hi") // data section의 0번째 주소부터 "Hi" 등록
      (func (export "writeHi")
          i32.const 0
          i32.const 2
          call $log))
    )
    
    function consoleLogString(offset, length){
      let bytes = new Uint8Array(memory.buffer, offset, length); // 자바스크립트는 인터프리터 언어니까 memory 객체 사용가능.
      let string = new TextDecoder('utf8').decode(bytes); // byte를 utf-8로 ??
      console.log(string);
    }
    let memory = new WebAssembly.Memory({initial:1});
    let importObject = {console : { log :consoleLogString }, js: {mem: memory}};
    WebAssembly.instantiateStreaming(fetch('test.wasm'), importObject)
    .then(result => {
      result.instance.exports.writeHi();
    })
    

wat 모듈에서의 table

  • wasm에서는 anyfunc 옵션을 추가할 수 있다. function의 signature가 어떠한 타입도 받을 수 있다는 뜻.
  • 하지만 anyfunc은 보안상의 이유로 linear memory에 저장할 수 없다.
  • linear memory는 original address를 사용하므로 임의로 접근을 혀용하면 안될 일이다.
  • 해결책으로 나온 것이 테이블에 함수 참조를 저장하고 테이블 인덱스를 전달하는 것으로 메모리 주소를 유출하는 것을 피할 수 있다.
  • linear memory를 data 섹션을 이용해서 초기화 하는 것처럼 elem 섹션을 사용하여 함수가 있는 테이블 영역을 초기화 할 수 있다.
    (module
      (table 2 anyfunc) // 2는 테이블의 초기 크기, anyfunc는 any signature의 함수.
      (elem (i32.const 0) $f1 $f2) // i32.const 0 은 인덱스의 시작부분을 나타낸다. f1, f2 함수를 테이블에 등록한다. 각각 인덱스는 0과 1
      (func $f1 (result i32)
          i32.const 42)
      (func $f2 (result i32)
          i32.const13)
    )
    

table 사용 시에 타입 체크하는 법

  • 테이블에 지정된 함수의 input output type들을 체크할 수 있는 방법은 type 노드를 사용하는 것이다.
  • 아래 코드는 함수가 i32 type을 리턴으로 하는지 체크하는 것이다.
  • call_indirect 함수는 stack에서 값을 하나 pop하여 해당 값을 index로 table에 있는 함수에 접근하여 call 한다.
  • 하나의 모듈에는 하나의 테이블만이 존재할 수 있다.
    (module
      (table 2 anyfunc)
      (func $f1 (result i32)
          i32.const 42)
      (func $f2 (result i32)
          i32.const 13)
      (elem (i32.const 0) $f1 $f2)
      (type $return_i32 (func (result i32)))
      (func (export "callByIndex") (param $i i32) (result i32)
          get_local $i
          call_indirect (type $return_i32)) // int형만 취급을 하나봄
    
    WebAssembly.instantiateStreaming(fetch('test.wasm'))
    .then(resolve => {
      console.log(resolve.instance.exports.callByIndex(0)); // 42
      console.log(resolve.instance.exports.callByIndex(1)); // 13
      console.log(resolve.instance.exports.callByIndex(2)); // error
    })
    

Mutating Tables and dynamic linking

  • 자바스크립트는 wasm 코드의 함수 참조에 대한 모든 접근 권한이 있다.
  • Grow(), get(), set() 명령어를 통해 테이블을 변형시킬 수 있고 get_elem(), set_elem()를 사용해서 테이블 자체를 바꿔버릴 수도 있다.
  • 테이블은 변경 가능하기 때문에 런타임 다이나믹 링크를 구현하는데 사용할 수 있다.
  • 프로그램이 다이나믹 링크되면 여러 인스턴스가 동일한 메모리 및 테이블을 공유할 수 있다.
  • 자바스크립트에서 모든 접근 권한이 있기 때문에 자바스크립트에서 만들고 wasm 코드에 import 해서 사용한다.
  • 아래 코드에서 i32.load와 i32.store는 memory에 저장되어 있는 값을 불러오고, 저장하는 역할을 한다. (data section 이겠지, stack처럼 쓸듯.)
  • 하나의 모듈에는 하나의 테이블이 들어간다.
  • 아래의 코드는 하나의 테이블이 여러 모듈에 공존하는 것이다.
  • shared0.wat에서 테이블에 속하는 함수가 정의되었고 export하지 않았다. shared1.wat에서 해당 함수를 콜한다. 테이블은 공유될 수 있기에 가능하다.
  • 테이블 뿐만 아니라 메모리도 공유되었기에 shared1.wat에서 저장한 값이 shared0.wat에서 불러와서 쓸 수 있는 것이다.
    // shared0.wat
    (module
      (import "js" "memory" (memory 1))
      (import "js" "table" (table 1 anyfunc))
      (elem (i32.const 0) $shared0func)
      (func $shared0func (result i32)
          i32.const 0
          i32.load) // memory 에서 값을 꺼내온다. index는 스택에서 pop하여 0을 가져온다.
    )
    // shared1.wat
    (module
      (import "js" "memory" (memory 1))
      (import "js" "table" (table 1 anyfunc))
      (type $void_to_i32 (func (result i32)))
      (func (export "doIt") (result i32)
          i32.const 0
          i32.const 42
          i32.store // stack에서 두개를 pop하고 첫번째 0 을 index로 거기에 42를 저장한다. // i32.store (i32.const 0) (i32.const 42) 로 적어도 된다.
          i32.const 0
          call_indirect (type $void_to_i32) // call_indirect 함수는 현재 이 모듈에 존재하는 테이블(1개)에 대한 접근을 한다.
    )
    // javascript
    const importObj = {
      js : {
          memory : new WebAssembly.Memory({ initial: 1}),
          table : new WebAssembly.Table({ initial: 1, element: "anyfunc" })
      }
    };
    Promise.all([
      WebAssembly.instantiateStreaming(fetch('shared0.wasm'), importObj),
      WebAssembly.instantiateStreaming(fetch('shared1.wasm'), importObj)
    ]).then(resolve => {
      console.log(results[1].instance.exports.doIt());
    })