用C/C ++进行Web汇编简介(译文)
By S.F.
本文链接 https://www.kyfws.com/news/introduction-to-web-assembly-with-c-cplusplus/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 15 分钟阅读 - 7086 个词 阅读量 0用C/C ++进行Web汇编简介(译文)
原文地址:https://www.codeproject.com/Articles/5250727/Introduction-to-Web-Assembly-with-C-Cplusplus
原文作者:Joel Ivory Johnson
译文由本站翻译
前言
使用C/C ++语言的Web程序集简介,第1部分.在这一部分中,我将介绍Web程序集,引导您完成开发工具的设置,并通过几个入门程序.
最近,我一直在利用Web Assembly.所有主要浏览器都支持它,它允许一个人使用为其他环境编写的已经存在的有用代码,并提供一些优于JavaScript的性能优势. Web Assembly具有很大的潜力和支持,我想向其他开发人员介绍它.在这篇文章中,我将使用C ++.但这绝对不是有人可以使用Web Assembly的唯一语言.在这篇文章中,我讨论了为什么有人可能想考虑Web组装以及如何进行开发环境设置.
什么是Web Assembly?
Web Assembly是在浏览器中运行的虚拟机的规范.与高度动态的JavaScript相比,Web Assembly可以实现更高的性能.与流行的误解相反,Web Assembly并未完全取代JavaScript.您可能会同时使用两者. Web Assembly基于LLVM(低级虚拟机),LLVM是编译器可以定位的基于堆栈的虚拟机.如果有人想制作一种新的编程语言,则可以让该语言的编译器生成LLVM代码,然后使用现有的工具链将其编译为平台特定的代码.为新语言构建编译器的人不需要为不同的CPU体系结构制作完全独立的系统.基于LLVM的Web程序集可以运行由多种语言编写的代码.当前,尚不支持垃圾回收,这限制了目前针对垃圾回收的语言. C/C ++,C#和Rust是目前可以与Web Assembly一起使用的几种语言,并且在将来会越来越多.
我可以使用哪些其他语言?
- 长生不老药
- 走
- 蟒蛇
- 锈
为什么要使用Web Assembly?
我建议Web Assembly主要是为了在计算量大的操作中提高性能.它使用的二进制格式比JavaScript严格得多,并且更适合于计算密集型操作.还有很多现有的经过测试的工作代码,例如人们可能希望在页面中使用的C/C ++中的密码术或视频解码器.尽管具有很大的灵活性,但是解释后的JavaScript代码的运行速度不像本地二进制文件那样快.对于某些类型的应用程序,这种性能差异并不重要(例如在文字处理器中).对于其他应用程序,性能差异会转化为体验差异. 虽然对性能的需求是制作本机二进制文件的动机,但也有安全方面的考虑.本地二进制文件可能比Web实现的解决方案有权访问更多的系统资源.确保程序(尤其是来自第三方的程序)不会进行任何恶意操作或未经许可访问资源可能会引起更多关注. Web Assembly帮助弥合这两个需求之间的鸿沟;它在沙箱中提供了更高性能的执行环境.
C ++?我不能因此导致缓冲区溢出吗?
当然.但是仅在运行代码的沙箱范围内.它可能会使您的程序崩溃,但不会导致沙箱外部代码的任意执行.还要注意,目前Web Assembly没有与Host API的任何绑定.当您将Web Assembly定位为目标时,您没有一个环境可以绕过JavaScript代码在其中运行的安全限制.没有直接访问文件系统的权限,也没有访问程序外部内存的权限,您仍然会被限制与不违反CORS限制的WebSockets和HTTP请求进行通信.
如何设置开发人员环境
Internet上有不同版本的说明,用于安装Web程序集工具.如果您运行的是Windows 10,则可能会遇到一系列说明,这些说明从告诉您安装Linux的Windows子系统开始.不要使用这些说明;我个人认为它们不必要地复杂.尽管我已经安装了Linux的Windows子系统并已将其运行用于其他目的,但这不是我希望编译Web Assembly代码的地方. 使用您选择的操作系统(Windows 10/8/7,macOS,Linux)克隆Emscripten git存储库,从中运行一些脚本,即可开始使用.这是要使用的命令.如果您使用的是Windows,请在命令开头省略" ./".
git https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
安装了工具后,您还需要设置一些环境变量.有执行此操作的脚本.在Windows 10上,运行:
emsdk_env.bat
对于其他操作系统,运行:
source emsdk_env.sh
这对环境变量所做的更新不是持久性的.它需要在下次重新启动时再次运行.对于编辑器,我建议使用Visual Studio Code.我将在本文的命令行中进行编译.随意使用您选择的编辑器.
Web Assembly Explorer
我不会在本文中的此工具中使用它,但是Web Assembly Explorer可以用作将C ++编译到Web Assembly中的在线工具,如果没有安装工具,则可以选择使用. https://mbebenita.github.io/WasmExplorer/
你好,世界
现在我们已经安装了工具,我们可以编译并运行某些东西.我们将做一个你好世界程序.键入以下源代码并将其保存在hello.cpp中.
#include
int main(int argc, char**argv)
{
printf("Hello World!\n");
return 0;
}
要从命令行编译代码,请键入以下内容:
emcc hello.cpp -o hello.html
编译器运行后,您将拥有三个新文件:
- 你好
- hello.html
- hello.js 如果您尝试直接打开HTML文件,则您的代码可能无法运行.相反,该页面必须通过HTTP服务器提供.如果已安装节点,请使用节点" http-server".您可以使用以下命令安装" http-server":
npm install http-server -g
然后,使用hello.html从目录启动服务器:
http-server . -p 81
在这里,我指示http-server
在端口81
上运行.您可以在此处使用您选择的端口,前提是没有其他端口正在使用它.记住,在其余所有说明中,请替换为您选择的端口.
打开浏览器,然后导航到http://localhost:81/hello.html.您将看到代码运行.如果您查看页面的源代码,则文件中会有很多"噪音".大部分噪音来自嵌入HTML中的显示图像.玩耍很好.但是,您将需要根据自己的需求进行定制.
我们可以提供一个shell或模板文件供编译器使用. Emscripten的最小文件位于https://github.com/emscripten-core/emscripten/blob/master/src/shell_minimal.html.下载该文件.它将用作我们的起点.为了将所有内容分发在一个文件中,这很方便.但是我不喜欢将CSS和JavaScript嵌入文件中.这里不需要CSS,并且CSS已被删除.我将JavaScript移至其自己的文件,并在HTML中添加了对其的脚本引用. HTML和脚本中有一些不必要的项目.让我们先来看一下脚本,然后开始使这个最小文件变得更加简约.
在脚本的顶部,页面元素包含三个变量,以指示下载和进度.这些不是绝对必要的.我要删除它们.我也需要删除对它们的引用. JavaScript的下面是一个名为setStatus的方法.我用对console.log()
的调用替换了它的主体,以打印传递给它的文本.我要编写的第一套程序不会使用画布.目前不需要该元素;我将其注释掉,而不是将其删除,以便以后使用.删除了该文件的前三行和引用它们的代码后,我将返回HTML.大部分被删除.我已注释掉画布参考. HTML文件中的一行带有文本" {{{SCRIPT}}}.编译器将把这个文件作为模板,并用对我们的Web Assembly文件特定的脚本的引用替换
{{{SCRIPT}}}}.
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Emscripten-Generated Code</title>
<link rel="stylesheet" href="./styles/emscripten.css" />
<script src="scripts/emscripten.js"></script>
</head>
<body>
<!--
<div class="emscripten_border">
<canvas class="emscripten" id="canvas"
oncontextmenu="event.preventDefault()"></canvas>
</div>
-->
<textarea class="emscripten" id="output" rows="8"></textarea>
{{{ SCRIPT }}}
</body>
</html>
当Web Assembly程序执行printf()
时,文本将被写入textarea
元素.我将hello.cpp文件放在这些文件中,然后使用以下命令对其进行编译.
emcc hello.cpp --shell-file shell_minimal.html -o hello.html
" –shell-file"参数指示要用作模板的文件. -o参数指示要写入的HTML文件的名称.如果查看hello.html,您会发现它与输入模板几乎相同.现在运行该站点,您将看到相同的结果,但界面更加简洁.再次运行该程序,您将看到一个更加简洁的界面,结果相同.
绑定功能
前面我提到过,Web程序集没有任何操作系统功能的绑定.浏览器也没有绑定.它也没有访问DOM的权限.取决于加载Web程序集以向其公开功能的页面.在emscripten.js中,“模块"对象定义了许多可用于Web程序集的功能.当C/C ++代码调用printf
时,它将通过此处定义的同名JavaScript函数传递.不需要名称相同,但是如果函数名称相同,则更容易跟踪它们.
从JavaScript调用C/C ++
但是,如果您希望绑定自己的函数,以便JavaScript代码可以调用C ++代码,该怎么办? “模块"对象有一个名为” ccall"的函数,可用于从JavaScript调用C/C ++代码,还有一个名为” cwrap"的函数可构建一个函数对象,我们可以保留该函数对象以重复调用同一函数.要使用这些功能,将需要一些其他的编译标志. 为了演示这两种从JavaScript调用C/C ++代码的方法的用法,我将在C ++代码中声明三个新函数.
- 无效的testCall()
- 串
- void printNumber(int num)
- int平方(int c) C ++语言执行所谓的名称处理;编译代码中的函数名称与未编译代码不同.对于要从C ++代码外部使用的函数,我们需要将函数的声明包装在extern" C"块中.如果我们的代码是用C而不是C ++编写的,则没有必要.由于该语言提供的某些功能,我仍然更喜欢C ++.通常,我在头文件中会有这样的声明.但就目前而言,我的C ++程序位于单个文件中.在程序顶部,我做了以下声明:
extern "C" {
void testCall();
void printNumber(int f);
int square(int c);
}
功能的实现是您所期望的.
void testCall()
{
printf("function was called!\n");
}
void printNumber(int f) {
printf("Printing the number %d\n", f);
}
int square(int c)
{
return c*c;
}
我的main
方法也有所变化.我必须包含一个新的头文件emscripten.h,因为我将要使用它提供的功能之一.在main
中,添加了以下行.
EM_ASM ( InitWrappers());
这将导致一个名为InitWrappers
()的JavaScript函数被调用.在下一节中,我将讨论
EM_ASM的工作方式.我将第三个<script/>元素添加到HTML文件中.第一个元素包含我的Emscripten提供的代码.第二个是在模板中存在{{{SCRIPT}}}
的位置插入的.第三个脚本标签如下.第三个脚本标签引用了包含InitWrappers
函数的JavaScript.
var testCall;
var printNumber;
var square;
function InitWrappers() {
testCall = Module.cwrap('testCall', 'undefined');
printNumber = Module.cwrap('testCall', 'undefined', ['number']);
square= Module.cwrap('square', 'number', ['number']);
}
我已经声明了三个将用于保存功能对象的变量.它们由cwrap
调用的返回值填充.在第一次" cwrap"调用中,参数是要调用的C/C ++函数的名称以及返回类型.此函数不返回任何值,这就是其返回类型设置为" undefined"的原因.在第二个调用中,传递了一个附加参数.列表中每个参数的类型.此函数仅接受一个参数,并且只需要一个包含一个元素的列表.在第三个调用中,将返回时间的参数设置为"数字",因为此方法将返回一个数值.为了调用这些函数,我将一些JavaScript添加到onclick
事件中.
此代码的compile语句不同.其中一些更改是可选的.但我将解释所有这些.
emcc hello.cpp --std=c++11 --shell-file shell_minimal.html
--emrun -o hello.html -s NO_EXIT_RUNTIME=1
-s EXPORTED_FUNCTIONS="['_testCall', '_printNumber','_square','_main']"
-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap','ccall']" -s WASM=1
- –std =c ++ 11
- –shell文件shell_minimal.html
- –emrun
- -o hello.html
- -s NO_EXIT_RUNTIME =1
- -s EXPORTED_FUNCTIONS =" ['_ testCall','_printNumber','_square']"
- 模组
- -s EXTRA_EXPORTED_RUNTIME_METHODS =" [‘cwrap’,‘ccall’]"
- 模组
- -s WASM =1
- ASM.js
从C/C ++调用JavaScript
我们已经在隐式地从C/C ++调用JavaScript.但是,让我们看一下如何从C/C ++显式调用JavaScript.有两种方法可以做到这一点;您可以将JavaScript代码直接嵌入C/C ++代码中,也可以使用函数emscripten_run_script()
.如果您曾经在C ++代码中嵌入过汇编语言,那么这两种方法中的第一种对您来说似乎并不陌生.
如果要在C ++代码中重复使用一段JavaScript代码,则可以使用EM_JS
用JavaScript编写一个函数.
EM_JS(void,myAlert,(), {
alert('hey, I am alerting you!');
console.log('you have been alerted.')'
});
int main() {
myAlert();
return 0;
}
由于此调用,名为myAlert()
的新函数可用.如果将JavaScript代码定义为只能使用一次,则可以使用EM_ASM
内联编写:
int main() {
EM_ASM(
alert('hey, I am alerting you!');
console.log('you have been alerted.')'
);
return 0;
}
我建议不要在您的C/C ++中嵌入很多代码.最多嵌入一个JavaScript函数调用可能更好.如果需要更新代码,则更新JavaScript函数比在C/C ++代码中进行更改和重建要容易得多.
C ++中的Sun Position
我想展示一个示例,该示例在关闭本文的第1部分之前做了不平凡的事情.我对天文计算很感兴趣.我决定采用C ++例程来计算太阳位置并将其用于网页中.经过快速的Google搜索,我发现了这一点:
- http://www.sci.fi/~benefon/azimalt.cpp
我必须进行一些更改才能使用它,但不是很多.原始例程直接在
main
中收集输入.我不需要在main
中做任何事情.我也不想使用cin对象.它会导致显示输入对话框.相反,我希望通过例程传递参数.我将保留cout
调用;他们 main函数只会在我修改代码时初始化包装器.我做了一个新的main
函数,该函数调用JavaScript函数执行初始化.
int main(void){
EM_ASM ( InitWrappers());
return 0;
}
原本的main函数已重命名为getSunInformation.我传递的是纬度,经度和时区信息,并删除了先前的cin用法,以提示用户输入此信息.
void getSunInformation(double latit, double longit, double tzone);
我还需要从此电话中获取信息.尽管有多种方法可以做到这一点,但现在我将采取一种简单的选择;我将传递参数的C ++代码调用JavaScript代码.我可以使用EM_ASM
来做到这一点.在此功能的早期使用中,我正在调用功能.现在我需要传递数据.在EM_JS中声明的JavaScript与C ++的作用域不同.它对C ++代码中的变量没有可见性.我们想要传递给JavaScript的任何信息都可以在参数中传递. JavaScript中的信息可以通过以美元符号开头,后跟位置参数数字的变量开头.第一个参数是$ 0,下一个是$ 1,第三个是$ 2,依此类推.
EM_ASM (
sunParameters($0,$1,$2, $3, $4, $4, $5, $6, $7);
sunNoonParams($8, $9);
sunCurrentPosition($10,$11);
,year,m,day, jd, latit, longit, tzone, delta*degs, daylen
,noont, altmax,
azim*degs,altit
);
我正在使用三个尚未见过的功能.尚未定义函数sun`Parameters'',
sunNoonParameters
和``sunCurrentPosition''.我制作了一个包含这些内容的新JavaScript文件.由脚本生成的JavaScript文件名为azimalt.js.我的JavaScript文件将添加到此文件;我已将其命名为azimAltPost.js.在这个文件中,我定义了InitWrappers函数和前面提到的三个sun函数.目前,
sun`函数会将接收到的参数写入控制台.传递给" getSunInformation"的两个值是美国佐治亚州亚特兰大地区的经度和纬度.如果您自己运行代码,则可能需要更改它们.
var getSunInformation;
function InitWrappers() {
getSunInformation = Module.cwrap('getSunInformation',
'undefined', ['number','number','number']);
getSunInformation(-84,34,-5);
}
function sunParameters(year, month, day, julianDate, latitude, longitude,
timeZone, delta, dayLength) {
console.log(`${year}-${month}-${day}`);
console.log(`Julian Date:${julianDate}`);
console.log(`Latitude:${latitude}, Longitude:${longitude}`);
console.log(`Time Zone: ${timeZone}`);
console.log('Delta: ${delta})');
console.log('Daylength: ${dayLength}')
}
function sunNoonParams(noont,altmax) {
console.log(`Noont: ${noont}, Altitude Max:${altmax}`);
}
function sunCurrentPosition(azim, alt) {
console.log(`Azimuth: ${azim} Altitude:${alt}`);
}
我在这里介绍一个新的编译参数,–post-js.此参数命名一个JavaScript文件,其内容将在脚本生成的代码之后运行.我将传递我的JavaScript文件作为此参数的值.我用来编译此命令的完整命令行如下:
emcc azimalt.cpp -o azimalt.html --post-js azimaltPost.js
-s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS="['_getSunInformation', '_main']"
-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap','ccall']" -s WASM=1
打开HTML文件(确保从Web服务器提供该文件),然后查看输出控制台.您应该看到有关您所在地区的日出和日落信息.
自定义日出和日落演示
该程序可以运行,但让我们在其中添加一些图形化的东西.我想添加一个24小时的模拟时钟,以一目了然地显示日出,日落和太阳相对于两者的当前位置.我还希望有一个指南针显示器来显示方位角,并有一个图形来显示高度角.我将使用SVG进行图形处理.许多元素都可以声明.我想出了以下UI.
在将其与Web程序集集成之前,我添加了一些范围滑块和JavaScript,以确保元素按我期望的方式移动.运行之后,我将WASM脚本复制到了新的HTML文件中,并以图形方式向我显示了今天的日出和日落时间.如果您想亲自查看,请访问https://j2i.net/apps/sunrise/.当我尝试将其托管在Web服务器上时,它最初失败了,因为服务器软件无法识别WASM扩展.如果遇到此类问题,请为扩展名注册mime类型.它是application/wasm
.
其他支持的图书馆
但是,无论是运行emcc –show-ports还是查看https://github.com/emscripten-ports,您都可以看到其他一些API enscripten编译器支持.在撰写本文时,这是在命令终端上运行命令的输出.
c:\shares\sdks\emsdk>emcc --show-ports
Available ports:
Boost headers v1.70.0 (USE_BOOST_HEADERS=1; Boost license)
icu (USE_ICU=1; Unicode License)
zlib (USE_ZLIB=1; zlib license)
bzip2 (USE_BZIP2=1; BSD license)
libjpeg (USE_LIBJPEG=1; BSD license)
libpng (USE_LIBPNG=1; zlib license)
SDL2 (USE_SDL=2; zlib license)
SDL2_image (USE_SDL_IMAGE=2; zlib license)
SDL2_gfx (zlib license)
ogg (USE_OGG=1; zlib license)
vorbis (USE_VORBIS=1; zlib license)
SDL2_mixer (USE_SDL_MIXER=2; zlib license)
bullet (USE_BULLET=1; zlib license)
freetype (USE_FREETYPE=1; freetype license)
harfbuzz (USE_HARFBUZZ=1; MIT license)
SDL2_ttf (USE_SDL_TTF=2; zlib license)
SDL2_net (zlib license)
cocos2d
regal (USE_REGAL=1; Regal license)
c:\shares\sdks\emsdk>
当您使用这些库之一时,emscripten编译器将检索该库,在本地构建它,并将其链接到您的项目.
第二部分是什么
我对本系列第二部分中的内容有一些想法,例如如何处理复杂的数据类型以及如何绑定到类.但我想听听您的消息.你想看见什么?在评论中分享您的想法和问题;我想在本系列的下一篇文章中对其中一些做出回应.
历史
- 日
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ C Intermediate 新闻 翻译