将一个C语言写的hello world编译成web版本是很简单的,网上有大量这样的例子。写这样的例子是必要的,让咱们可以快速入门,可是实际项目则要复杂的多,这里会介绍一些emscripten的基础知识,同时强调移植AWTK过程当中遇到的问题,但愿能让你们少走弯路。web
-s EXPORTED_FUNCTIONS="['_awtk_web_init']"
函数名前面要加下划线,好比函数名为awtk_web_init,导出的名称则为_awtk_web_init。json
对于小的项目,导出的函数不多,直接写在命令行也是能够的。对于大的项目,导出的函数不少,应该把内容写到文件中,经过@符合告诉emcc从文件中读取导出的函数,这样维护起来会方便不少。如:canvas
-s EXPORTED_FUNCTIONS=@configs/export_app_funcs.json
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" ]
-s EXTRA_EXPORTED_RUNTIME_METHODS ="['cwrap']"
同理,将它的内容放在文件中,也是更可取的方法。如:app
-s EXTRA_EXPORTED_RUNTIME_METHODS=@configs/export_runtime_funcs.json
configs/export_runtime_funcs.json的内容:异步
[ "ccall", "cwrap", "addFunction", "removeFunction", "addOnPostRun", "addOnInit", "addOnExit", "addOnPreMain", "UTF8ToString" ]
对于大的项目,调试版本最好不要加-g标志,产生的代码实在太大了,可能让浏览器处于假死状态,根本无法调试。用缺省生成的代码调试基本上就OK了。函数
发布版本建议加-Os,代码体积会大大减少,并且它会把数据独立出来,提升加载的速度。oop
emcc生成的常量数据的首地址并不会按32bit/64bit对齐,AWTK就踩到这个坑里了,SAFE_HEAP宏有助于发现这个问题。建议定义下列这些宏:优化
-DSAFE_HEAP=1 -DASSERTIONS=1 -DSTACK_OVERFLOW_CHECK=1
后来我给AWTK的常量全加上了对齐的属性:this
#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*/
对于一个大型项目,在Windows平台下,命令行参数很容易超长。最简单的办法就是将emcc的参数写入文件中,经过@符合告诉emcc从文件读取。如:
emcc -v @args.txt
包含emscripten.h头文件,它提供了一些方法,让C语言调用JS的函数。
#include <emscripten.h>
如:
emscripten_run_script("alert('hi')");
如:
#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; }
这是最快也是最简单的调用方式,AWTK里基本上都是采用这种方式调用的。如:
C端代码:
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端代码:
VGCanvas.save = function () { VGCanvas.ctx.save(); return true; }
**传递数值参数也很是简单。**如:
C端代码:
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端代码:
VGCanvas.moveTo = function (x, y) { VGCanvas.ctx.moveTo(x, y); return true; }
传递字符串参数就麻烦一点了。如:
C端代码:
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端代码:
VGCanvas.setTextAlign = function (value) { VGCanvas.ctx.textAlign = pointerToString(value); return true; }
字符串参数传递到JS函数里时,JS函数拿到的内存地址的偏移量,须要把它解码出来,生成JS的字符串对象。pointerToString函数是这样实现的:
function pointerToString(pointer) { return pointer && Module.UTF8ToString(pointer, 1024) || null; }
Module.UTF8ToString函数须要在前面介绍的EXTRA_EXPORTED_RUNTIME_METHODS中导出才能使用。
AWTK中还用了二进制数据做为参数,网上没有见到相关的例子,只好本身去看代码研究了。AWTK里须要把位图数据(rgba颜色值),传递到JS中,再设置到画布里。具体作法是这样的:
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; }
mutableImage.addr是rgba数据的地址,它是用malloc分配出来的,我看了malloc函数的实现,它就相对于Module.HEAP32的字节数偏移量。因为HEAP32是4字节数据,在做为偏移量使用时,须要右移2位:
let start = mutableImage.addr >> 2;
再经过subarray从HEAP32中获取这段数据:
let array = Module.HEAP32.subarray(start, end);
另外这里值得一提的是,imageData.data是Int8Array,要转换成Int32Array,能够用下列方式:
let imageData = ctx.getImageData(0, 0, w, h); let data = new Int32Array(imageData.data.buffer);
按下面这种方式,则是把每个元素从8bit扩展成32bit了。
let imageData = ctx.getImageData(0, 0, w, h); let data = new Int32Array(imageData.data);
要在JS里调用C的函数,通常用Module.cwrap包装一下,它须要提供如下参数:
参数和返回值的类型有:
如:
Awtk._onImCommit = Module.cwrap('awtk_web_on_im_commit', 'number', ['string', 'number']); Awtk.onImCommit = function (text, timestamp) { return Awtk._onImCommit(text, timestamp); }
常见的用法的在文档中都有清楚的说明,这里再也不赘述。若是参数是一个回调函数,就稍微麻烦一点。
1.要导出addFunction/removeFunction(参考前面)
2.要指定参数RESERVED_FUNCTION_POINTERS。
如:
-s RESERVED_FUNCTION_POINTERS=1000
如:
widget_on(this.nativeObj, type, Module.addFunction(wrap_on_event(on_event)), ctx);
最麻烦的是函数用完以后,要调用removeFunction把函数从表里移出,对于同步调用的回调函数这没有什么问题,可是对异步调用函数,特别是屡次调用的异步函数,何时能够移出只有C代码里才知道,因此须要在C代码里添加处理。如:
#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; }