Portable Notes for AWTK WEB Edition - Base

Keywords: JSON less Windows C

Compiling a hello world written in C into a web version is easy, and there are many examples on the web.Writing such an example is necessary to get us started quickly, but the actual project is much more complex. Here we will introduce some basic knowledge of emscripten and highlight the problems encountered during the migration of AWTK, in the hope that you will take less detours.

1. Command-line parameters

  • 1.EXPORTED_FUNCTIONS is used to export functions of C in the application for JS calls.For example:
-s EXPORTED_FUNCTIONS="['_awtk_web_init']"

Underline the function name, such as awtk_web_init for the function name and _awtk_web_init for the exported name.

For small projects, few functions are exported, and it is possible to write them directly on the command line.For large projects, there are many exported functions. You should write the contents to a file and tell emcc to read the exported functions from the file through @Compliance, which makes maintenance much easier.For example:

-s EXPORTED_FUNCTIONS=@configs/export_app_funcs.json

Contents of configs/export_app_funcs.json:

[
    "_awtk_web_init",
    "_awtk_web_deinit",
    "_awtk_web_main_loop_step",
    "_awtk_web_on_key_down",
    "_awtk_web_on_key_up",
    "_awtk_web_on_wheel",
    "_awtk_web_on_pointer_down",
    "_awtk_web_on_pointer_move",
    "_awtk_web_on_im_commit",
    "_awtk_web_on_pointer_up"
]
  • 2.EXTRA_EXPORTED_RUNTIME_METHODS is used to export functions in runtime for JS calls.For example:
-s EXTRA_EXPORTED_RUNTIME_METHODS ="['cwrap']"

Similarly, placing its contents in a file is a more preferable approach.For example:

-s EXTRA_EXPORTED_RUNTIME_METHODS=@configs/export_runtime_funcs.json

Contents of configs/export_runtime_funcs.json:

[
    "ccall",
    "cwrap",
    "addFunction",
    "removeFunction",
    "addOnPostRun",
    "addOnInit",
    "addOnExit",
    "addOnPreMain",
    "UTF8ToString"
]
  • 3. Debugging and optimization

For large projects, it is best not to use the -g flag for debugging versions. The resulting code is too large and may leave the browser in a false dead state to debug at all.Debugging code generated by default is basically OK.

The release suggests adding-Os, which will reduce the size of the code considerably, and it will isolate the data and increase the loading speed.

  • 4. Debugging macros.

The first address of the constant data generated by emcc is not aligned to 32bit/64bit. AWTK stepped into this pit. The SAFE_HEAP macro helps to find out.It is recommended that the following macros be defined:

-DSAFE_HEAP=1 -DASSERTIONS=1 -DSTACK_OVERFLOW_CHECK=1

Later, I added alignment properties to the constants of AWTK:

#ifdef _MSC_VER
#define TK_CONST_DATA_ALIGN(v) __declspec(align(8)) v
#else
#define TK_CONST_DATA_ALIGN(v) v __attribute__((aligned(8)))
#endif /*_MSC_VER*/
TK_CONST_DATA_ALIGN(const unsigned char data_a_b_c_any[]) = { 
0x08,0x00,0x00,0x01,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x61,0x2d,0x62,0x2d,0x63,0x2e,0x61,0x6e,
0x79,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x61,0x62,0x63,0x0a,0x00,0x00,0x00,0x00,};/*52*/
  • 5. Long command line parameters

For a large project, command line parameters can easily be too long on the Windows platform.The easiest way is to write the parameters of emcc to a file and tell emcc to read from it through @conformance.For example:

emcc -v @args.txt

2. Functions calling JS in C language

  • 1. Header file

Contains the emscripten.h header file, which provides methods for C to call JS functions.

#include <emscripten.h>
  • 2. Called through emscripten_run_script.

For example:

emscripten_run_script("alert('hi')");
  • 3. Call through EM_JS/EM_ASM.

For example:

#include <emscripten.h>

EM_JS(void, call_alert, (), {
  alert('hello world!');
  throw 'all done';
});

int main() {
  call_alert();
  return 0;
}
#include <emscripten.h>

int main() {
  EM_ASM(
    alert('hello world!');
    throw 'all done';
  );
  return 0;
}
    1. Called through a macro such as EM_ASM_INT.

This is the fastest and easiest way to invoke, which is basically the way to invoke in AWTK.For example:

C-side code:

static ret_t vgcanvas_web_save(vgcanvas_t *vgcanvas) {
  int32_t ret = EM_ASM_INT({ return VGCanvas.save(); }, 0); 

  return ret ? RET_OK : RET_FAIL;
}

JS-side code:

VGCanvas.save = function () {
  VGCanvas.ctx.save();

  return true;
}

** Passing numerical parameters is also very simple.**For example:

C-side code:

static ret_t vgcanvas_web_move_to(vgcanvas_t *vgcanvas, float_t x, float_t y) {
  EM_ASM_INT({ return VGCanvas.moveTo($0, $1); }, x, y);
    
  return RET_OK;
}   

JS-side code:

VGCanvas.moveTo = function (x, y) {
  VGCanvas.ctx.moveTo(x, y);
  
  return true;
} 

Passing string parameters is a little more cumbersome.For example:

C-side code:

static ret_t vgcanvas_web_set_text_align(vgcanvas_t *vgcanvas,
                                         const char *text_align) {
  EM_ASM_INT({ return VGCanvas.setTextAlign($0); }, text_align);
  return RET_OK;
} 

JS-side code:

VGCanvas.setTextAlign = function (value) {
  VGCanvas.ctx.textAlign = pointerToString(value);

  return true;
}

When a string parameter is passed into a JS function, the offset of the memory address that the JS function gets needs to be decoded to generate a JS string object.The pointerToString function is implemented as follows:

function pointerToString(pointer) {
  return pointer && Module.UTF8ToString(pointer, 1024) || null;
}

The Module.UTF8ToString function needs to be exported in the EXTRA_EXPORTED_RUNTIME_METHODS described earlier to work.

AWTK also uses binary data as a parameter. There are no relevant examples on the Internet, so I have to go to the code research for myself.In AWTK, bitmap data (rgba color values) are passed to the JS and then set to the canvas.This is done as follows:

VGCanvas.updateMutableImage = function (id) {
  let mutableImage = ImageCache.get(id);

  let w = mutableImage.width;        
  let h = mutableImage.height;       
  let size = mutableImage.width * mutableImage.height;
  let start = mutableImage.addr >> 2;
  let end = start + size; 
  let array = Module.HEAP32.subarray(start, end);
  let ctx = mutableImage.getContext('2d');
  let imageData = ctx.getImageData(0, 0, w, h);
  let data = new Int32Array(imageData.data.buffer);

  for(let i = 0; i < size; i++) {
    data[i] = array[i];
  }                                         
  ctx.putImageData(imageData, 0, 0, 0, 0, w, h);

  return true;
}

The mutableImage.addr is the address of the rgba data, which is allocated using malloc. I see the implementation of the malloc function, which is offset from the number of bytes in Module.HEAP32.Since HEAP32 is 4 bytes of data, it needs to be offset 2 bits to the right when used as an offset:

  let start = mutableImage.addr >> 2;

This data is then retrieved from HEAP32 by subarray:

  let array = Module.HEAP32.subarray(start, end);

Also worth mentioning here is that imageData.data is an Int8Array and can be converted to an Int32Array in the following ways:

  let imageData = ctx.getImageData(0, 0, w, h);
  let data = new Int32Array(imageData.data.buffer);

In this way, each element is expanded from 8 bits to 32 bits.

  let imageData = ctx.getImageData(0, 0, w, h);
  let data = new Int32Array(imageData.data);

3. JS Calls C Functions

To call C's function in JS, it is usually wrapped in Module.cwrap, which requires the following parameters:

  • Function name
  • Return value
  • parameter list

The types of parameters and return values are:

  • number
  • string
  • array

For example:

Awtk._onImCommit = Module.cwrap('awtk_web_on_im_commit', 'number', ['string', 'number']);

Awtk.onImCommit = function (text, timestamp) {
  return Awtk._onImCommit(text, timestamp);
}

Common usages are clearly documented, and will not be repeated here.If the parameter is a callback function, it is slightly more cumbersome.

  • 1. To export addFunction/removeFunction (see previous)

  • 2. To specify the parameter RESERVED_FUNCTION_POINTERS.

For example:

-s RESERVED_FUNCTION_POINTERS=1000
  • 3. Call addFunction to convert the function to a number and pass in as a parameter.

For example:

widget_on(this.nativeObj, type, Module.addFunction(wrap_on_event(on_event)), ctx);

The most troublesome thing is to call removeFunction to remove the function from the table after the function has been exhausted. There is no problem with callback functions that are called synchronously, but when can asynchronous calling functions, especially asynchronous functions that are called multiple times, be moved out of only the C code, so you need to add a place in the C codeReason.For example:

#ifdef AWTK_WEB_JS
#include <emscripten.h>
#endif /*AWTK_WEB_JS*/

static ret_t emitter_item_destroy(emitter_item_t* iter) {
  if (iter->on_destroy) {
    iter->on_destroy(iter);
  }

#ifdef AWTK_WEB_JS
  EM_ASM_INT({ return TBrowser.releaseFunction($0); }, iter->handler);
#endif /*AWTK_WEB_JS*/

  memset(iter, 0x00, sizeof(emitter_item_t));
  TKMEM_FREE(iter);

  return RET_OK;
}

Posted by dreams4000 on Wed, 11 Sep 2019 16:50:02 -0700