Emscripten代码移植之代码可移植性与限制(一)

Emscripten代码移植之代码可移植性与限制(一)

翻译:云荒杯倾

Emscripten代码移植主题涵盖了将C、C++代码移植到Emscripten时需要考虑的所有核心考虑问题,以及一般的编码和调试指南。
共有以下主题。

代码可移植性与限制、emscripten的运行环境、连接C++与JavaScript、文件和文件系统、多媒体数据和图形、调试、多线程编程支持、移植SIMD代码、Asyncify、Emterpreter

每一部分内容都比较多,本文主要讲第一部分,代码可移植性与限制。下面是正文:

代码可移植性与限制

Emscripten几乎可以编译任何可移植的c/c++代码到JavaScript。但由于浏览器环境限制和Emscripten编译出来的代码的限制,一些代码为了能被编译需要做改动,本文就帮我们找出这部分代码。

1、关于可移植性的指导


本节解释了哪些类型的代码是不可移植的(或者更难于移植);哪些代码可以编译,但会运行得很慢。开发人员可以使用这些信息来评估移植代码和重写代码的工作量。

1.1 不能编译的代码

为了使Emscripten工作,下面类型的代码需要重写。(理论上,在使用模拟的情况下,可以使用Emscripten解决这些问题,但速度非常慢。)

  • 代码是多线程的,并使用共享状态。JavaScript有线程(web workers),但它们不能共享状态。它们传递消息postMessage()。

    1
    2
    Note:
    如果JavaScript标准机构将共享状态添加到webworker中,支持多线程代码将成为可能。
  • 代码依赖于大端序架构。Emscripten编译的代码目前需要一个little-endian主机运行,这种主机占了连接到互联网的99%的机器。这是因为JavaScript类型化数组服从主机字节序,LLVM需要知道目标字节序是什么。

  • 依赖于x86对齐方式的代码。x86允许未对齐的内存读写(例如,您可以从一个非偶数地址读取一个16位的值),但是其他的架构不是。对于emscripten生成的JavaScript,其内存对齐方式是未定义的。如果您使用
    SAFE_HEAP = 1构建您的代码,那么您将得到一个清晰的运行时异常,参见调试

  • 使用本机环境的底层特性的代码,例如setjmp / longjmp涉及的本地堆栈操作。(we support proper setjmp/longjmp, i.e.,
    jumping down the stack, but not jumping up to an unwound stack, which is undefined behavior).

  • 扫描寄存器或堆栈的代码。因为在寄存器或堆栈上的变量可能是被放置到一个并不能被扫描的js局部变量里保存的。

1
2
NOTE:
如果你是一个喜欢自己写垃圾回收程序的程序员,可能对这类代码比较熟。。。
  • 具有特定于体系结构的内联汇编(比如包含x86代码的asm())的代码是不可移植的。这段代码需要用可移植的C或C++来替换。有时,代码库会将可移植的代码和可选的内联程序集写在一起作为优化,你需要找到一个选项使内联汇编代码不可用。

1.2 能编译但是运行得比较慢的代码

1
2
Note:
当你要优化代码的时候,就会知道了解这些事项是有用的。

下面类型的代码会被编译,但是可能运行的很慢:

  • 64位整型变量。数学运算(+,-,*,/)是慢的,因为它们是被模拟的。这是因为JavaScript没有本地64位int类型,因此这是不可避免的。

  • C++异常。在JavaScript中,这些代码通常会使JavaScript引擎关闭各种优化。因此,在- o1和上面的默认情况下,异常会被关闭。要重新启用它们,请运行emcc与- s DISABLE_EXCEPTION_CATCHING= 0。

  • setjmp also prevents relooping around it,迫使我们使用一种效率较低的方法来模拟控制流。

2、API限制


浏览器环境和JavaScript不同于C/C++通常运行的本地环境。这些差异对如何调用和使用本地API施加了一些限制。本部分列出了一些比较明显的限制。

2.1 网络

Emscripten支持libc库的网络函数,但您必须限制他们是异步(非阻塞)操作。这是因为底层的JavaScript网络函数是异步的。

2.2 文件系统

Emscripten支持libc文件系统函数,C /C++代码可以以正常方式编写。

在浏览器环境中运行的代码是沙盒sandboxed,并且不直接访问本地文件系统。然后,Emscripten就创建了一个虚拟文件系统,它可以预装数据,或者链接到url来懒加载。这会影响同步文件系统函数调用以及一个项目如何被编译。关于这方面,请参见文件系统概述

2.3 应用程序主循环

浏览器事件模型使用合作模式的多任务处理——每个事件都有一个运行的“turn”,然后必须将控制权返回给浏览器事件循环,这样其他事件就可以处理了。
HTML页面挂起的一个常见原因是JavaScript未完成并且未将控制权返回给浏览器。

这将影响含有死循环的主循环的应用程序的代码编写。有关更多信息,请参见Emscripten Runtime环境

3、函数指针的问题


函数指针有三个主要的问题:
1、指针类型转换会引起指针调用失败。
针对函数声明时的签名不同,函数指针会被存储到不同的表中。当一个函数被调用时,它会在与当前函数指针签名关联的表中搜索它。如果你进行了指针类型转换,而指针和所有的表并没有被修改,
则调用代码将在错误的表中查找。而错误的表中实际上很可能并没有一个叫该名字的指针,这样就出错了。

例如,一个声明为int(int)(返回int,接收int)的函数,会被添加到表FUNCTION_TABLE_ii。如果您将一个指向该函数指针投射到void(int)(不返回,接收int),
那么代码将在FUNCTION_TABLE_vi中查找函数。

你可能看到编译警告:

1
warning: implicit declaration of function

推荐的解决方案是重构代码以避免这种情况,如下面的Asm指针转换所描述的那样。

2、当你使用-o2以及更高优化级别的时候,比较不同类型的函数指针会产生错误的结果,而错误的函数指针可能更具误导性。要检查你的代码
出问题的原因,可以将aliasing_function_pointer设为零,(- s aliasing_function_pointer= 0)进行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
NOTE:
在asm.js中,函数指针存储在特定函数类型的表中。如FUNCTION_TABLE_ii。
在较低级别的优化中,每个函数指针在所有函数类型表上都有一个惟一的索引值(一个函数指针只在其中一个表的某个索引位置存在,在所有其他表中
这个索引位置都是一个空槽)。因此,比较函数指针(索引)能给出了一个准确的结果,但如果是试图在错误的表中调用函数指针,将会抛出一个错误,因为该索引是空的。
在-o2和更高级别的优化设置下,表被优化,以至于所有函数指针都在顺序索引中。这是一个有用的优化,因为如果没有所有空槽,表就更紧凑,
但它确实意味着函数索引不再是“全局”的惟一(因为一个函数指针在这张表中的索引位置与在另一张表中的索引位置不同了)。此时需要一张特定的表
和在这样表中的特定位置索引才能够唯一索引到一个函数。
因此,高级别的优化编译:
1、由于不同类型的函数可以有相同的索引(尽管在不同的表中),函数指针的比较可能会产生错误的结果。
2、函数指针代码中的错误更难于调试,因为它们导致错误的代码被调用,而不是显式的错误(就像在表中的“漏洞”中那样)。

3、结构体按值传递时,老版本的clang会为c和c++代码生成两种不同的代码,这两种格式的代码不兼容,你可能会收到一个警告。
解决方案是按引用传递结构体,或者不要在有结构体的位置混淆c和c++(比如,重命名.c为.cpp)。

Asm指针转换

如上所述,在asm.js模式下,函数指针必须使用正确的类型调用,否则调用将失败。这是因为在函数声明的时候,每个函数指针会基于这个函数的签名被存储在一个特定的表中: 将指针转换为另一个类型会导致调用代码在错误的位置(表)查找函数指针。

1
2
NOTE:
对于每种类型的函数指针都有一个单独的表,可以让JavaScript引擎知道每个函数指针调用的确切类型,这样也好进而优化他们。

有三种解决办法,优先选择第二种:

  • 调用者在函数指针被调用之前把指针类型再转回原来的指针类型,这是有问题的因为这需要调用者知道它原来的类型是什么,而调用者实际上并不知道一个指针之前的类型是什么。
  • 创建一个不需要转换的适配器函数,从适配器函数调用原始函数。
  • 使用EMULATE_FUNCTION_POINTER_CASTS。当你使用-s EMULATE_FUNCTION_POINTER_CASTS=1编译时,Emscripten将发出代码来模拟运行时的函数指针类型转换,
    添加额外的参数/删除参数/更改参数类型/添加或删除返回类型等。这可以增加显著的运行时开销,因此不推荐,但值得尝试。

4、特定浏览器限制


本页面列出了一些 与Emscripten编译出来的应用程序和游戏相关的 主要浏览器的最新版本之间的差异:

  • 函数emscripten_get_now()以毫秒的形式返回一个wallclock time。
    Opera 12.16和Windows谷歌Chrome 28.0.1500.95有一个限制,即计时器的精度仅为毫秒。
    在其他主流浏览器上(IE10,firefox22,非windows的Chrome 28),也都是亚毫秒精度。

  • WebGL并没有在Internet Explorer得到完全支持(至少在IE12之前)。

  • Opera 12.16对W3C的File API的支持有限。特别是它不支持createObjectURL函数,
    这意味着不可能使用浏览器的图像编解码器来解码Emscripten虚拟文件系统中的预加载文件。

  • Emscripten中OpenAL和SDL audio的支持依赖于Web Audio API


Emscripten代码移植系列文章

Emscripten代码移植主题系列文章是emscripten中文站点的一部分内容。
本文是第一个主题。
第一个主题介绍代码可移植性与限制
第二个主题介绍Emscripten的运行时环境
第三个主题第一篇文章介绍连接C++和JavaScript
第三个主题第二篇文章介绍embind
第四个主题介绍文件和文件系统
第六个主题介绍Emscripten如何调试代码