编译wasm Web应用
刚学完WebAssembly的入门课,卖弄一点入门知识。
首先我们知道wasm是目标语言,是一种新的V-ISA标准,所以编写wasm应用,正常来说不会直接使用WAT可读文本格式,更不会用wasm字节码;而是使用其他高级语言编写源代码,经过编译后得到wasm应用。课程中使用了C++来编写源代码,所以这里我也用C++来编写demo。
wasm的运行环境主要分为两类,一类是Web浏览器,另一类就是out-of-web环境,运行于Web浏览器的wasm应用主要使用Emscripten来编译得到,因为它会在编译过程中,为所编译代码在Web平台的功能适配性进行一定的调整。
针对Web平台的编译
对于功能适配性的调整,可以从下面这个例子中得到体现。
编码
首先我们编写一段功能简单的C++源代码:
#include <iostream>
extern "C" {
// 防止Name Mangling
int add(int x, int y) {
return x + y;
}
}
int main(int argc, char **argv) {
std::cout << add(10, 20) << std::endl;
return 0;
}
这段代码里,声明了一个函数“add”,它的定义被放置在“extern "C" {}
”结构中,以防止函数名被C++的Name Mangling机制更改,从而确保在宿主环境中调用该函数时,可以用与C++源码中保持一致的函数名,来直接调用这个函数。
这段代码中还定义了主函数main,其内部调用了add函数,并且通过std::cout
来将该函数的调用结果输出到stdout
。
编译
现在我们可以用Emscripten这个工具集中最为重要的编译器组件emcc,来编译这段源代码。命令如下所示:
emcc main.cc -s WASM=1 -O3 -o main.html
通过“-s”参数,为emcc指定了编译时选项“WASM=1”,这样emcc就会将输入的源代码编译为wasm格式目标代码,“-o”参数则指定了产出文件的格式为“.html”,这样Emscripten就会生成一个可以直接在浏览器中使用的Web应用。
这个自动生成的应用中,包含了wasm模块代码、JavaScript代码以及HTML代码。
运行
现在我们可以尝试在本地运行这个简单的Web应用。首先自行准备一个简单的Web服务器:
const http = require('http');
const url = require('url');
const fs = require('fs');
const path = require('path');
const PORT = 8888;
const mime = {
"html": "text/html;charset=UTF-8",
"wasm": "application/wasm" // 遇到".wasm"格式文件的请求时,返回特定的MIME
}
http.createServer((req, res) => {
let realPath = path.join(__dirname, `.${url.parse(req.url).pathname}`);
// 检查所访问文件是否存在并且可读
fs.access(realPath, fs.constants.R_OK, err => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end();
} else {
fs.readFile(realPath, "binary", (err, file) => {
if (err) {
// 文件读取失败时返回500
res.writeHead(500, { 'Content-Type': 'text/plain' });
end();
} else {
// 根据请求的文件返回相应的文件内容
let ext = path.extname(realPath);
ext = ext ? ext.slice(1) : 'unknow';
let contentType = mime[ext] || 'text/plain';
res.writeHead(200, { 'Content-Type', contentType });
res.write(file, "binary");
res.end();
}
});
}
});
}).listen(PORT);
console.log("Server is running at port: " + PORT + ".");
这段代码中最为重要的一个地方,就是对wasm格式文件请求的处理。
通过返回特殊的MIME类型“application/wasm”,我们明确告诉浏览器,这是一个wasm格式的文件,这样浏览器就可以允许应用使用针对wasm文件的“流式编译”方式,来加载和解析该文件。
现在我们通过8888端口来访问刚刚编译生成的main.html文件。
可以看到,Emscripten将C++源码中使用std::cout
将数据输出到stdout
,模拟为输出到页面上指定的textarea区域。这就是Emscripten针对Web平台的功能适配性调整。
再继续看,Emscripten自动生成的完整wasm Web应用,不管是js文件还是html文件,体积都偏大,这是因为Emscripten自动生成的“胶水代码”中,包含有通过JavaScript模拟出的POSIX运行时环境的完整代码,而大多数情况下,我们不需要这些。
仅生成wasm模块
那怎样可以使得Emscripten仅生成wasm模块,而js胶水代码和Web API这两部分的代码由我们自己编写呢?
答案就是调整编译时的命令行参数。那么我们要如何去编写JS来调用wasm模块导出的函数呢?
课程里有个图像处理的例子,这里就来整个小例子。
首先编写我们的HTML页面:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DEMO</title>
</head>
<body>
<div>
<h1>Counter: </h1>
<span>0</span>
<button id="increaseButton">点我+1</button>
</div>
<script src="index.js"></script>
</body>
</html>
这里想要实现一个功能,点击按钮后,span内的数字加1,当然这个功能JavaScript也能做,但现在作为练习,我们要通过调用wasm函数来实现。
然后就是重要的JavaScript代码,如下:
// index.js
document.addEventListener('DOMContentLoaded', async () => {
let response = await fetch('./index.wasm');
let bytes = await response.arrayBuffer();
let {instance} = await WebAssembly.instantiate(bytes);
let {
increase
} = instance.exports;
const span = document.querySelector('span');
const button = document.querySelector('#increaseButton');
let count = 0;
button.addEventListener('click', () => {
count = increase(count);
span.innerText = count;
});
});
首先,通过fetch获取wasm模块,并获取fetch方法返回的Response对象;
然后,调用response对象上的arrayBuffer()方法,将内容解析为ArrayBuffer的形式,这个ArrayBuffer将作为WebAssembly.instantiate方法的实际调用参数;这是一个用于实例化wasm模块的方法。
接着,WebAssembly.instantiate将实例化对应的wasm模块,我们就可以获得模块的实例对象,在instance变量中,可以获得从wasm模块导出的所有方法。
此时,我们就可以调用wasm模块的方法了,假设instance上有个increase方法,就可以这样调用。
现在,我们编写对应的C++代码并进行编译。
// index.cc
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE int increase(int x) {
return x+1;
}
}
此处我们需要引入<emscripten.h>
,因为需要使用其中定义的宏EMSCRIPTEN_KEEPALIVE
,因为这个文件中我们不声明主函数main,也不在文件内部调用这个increase函数,为了防止在编译过程中被DCE(Dead Code Elimination)处理掉,需要使用这个宏来标记函数。
现在我们来编译这个文件。
$ emcc index.cc -s WASM=1 -O3 --no-entry -o index.wasm
仅生成wasm模块文件的编译方式,通常称为”standalone模式”。
“-o”参数为我们指定了输出的文件格式为“.wasm”,这就是告诉Emscripten以“standalone”的方式来编译C++源码。
“--no-entry”参数则告诉编译器,这个wasm模块没有声明“main”函数。
上述命令执行完毕后,就会得到一个名为“index.wasm”的二进制模块文件。
此时我们就可以尝试去运行这个Web应用,可以看到和期待的效果一致。
当然这个demo很简单,目前要发挥wasm的优势,更适合将其应用在计算密集的功能。
调试应用
当我们编写完应用时,少不了要调试。那么如何针对wasm应用进行调试呢,Emscripten也提供了一些方式。
编译阶段
首先是针对编译阶段,当使用emcc编译项目时,可以通过为命令添加“EMCC_DEBUG”环境变量的方式,来让emcc以“调试模式”来编译项目。
$ EMCC_DEBUG=1 emcc index.cc \
> -s WASM=1 \
> -O3 \
> --no-entry -o index.wasm
可以看到编译时输出了很多的信息,这是因为我们将EMCC_DEBUG这个环境变量的值设置为1,EMCC_DEBUG的值可以设置为3个值,分别是0、1、2。
0表示关闭调试模式,这和不加这个环境变量是一样的效果;1表示输出编译时的调试性信息,同时生成包含有编译器各个阶段运行信息的中间文件;可用于编译流程的调试。
可以通过ls
命令查看生成了哪些文件;调试性信息中包含了各个编译阶段所实际调用的命令行信息,通过对这些信息分析,能够辅助开发者查找编译失败的原因。
当EMCC_DEBUG的值设置为2时,可以得到更多的调试性信息。
运行阶段
当我们成功地编译了wasm应用,但在实际运行时发生了错误,就需要在运行时进行调试。Emscripten也提供了一定的支持,我们可以在编译时设定参数“-g“以保留与调试相关的信息。
当设置为”-gsource-map“时,emcc会生成可用于在Web浏览器中进行“源码级”调试的特殊DWARF信息;通过这些特殊格式的信息,使我们可以直接在浏览器中对wasm模块编译之前的源代码进行诸如“设置断点”、“单步跟踪”等调试手段。
这里我们尝试调试之前编写的index.cc。
$ emcc index.cc -gsource-map -s WASM=1 -O3 --no-entry -o index.wasm
此时重新加载Web应用并打开“开发者面板”的“sources”Tab,就可以通过“操作”C++源代码的方式,来为应用所使用的wasm模块设置断点。(wasm模块的加载方式需要改为“流式编译”)。
通过这种方式,开发者就可以方便地在wasm Web应用的运行过程中,调试发生在wasm模块内部的“源码级”错误。
WebAssembly作为一种相对较新的技术,可以先保持一点了解。
编译wasm Web应用的更多相关文章
- [CI] 使用Jenkins自动编译部署web应用
写在前面 初步接触持续集成自动化过程,本篇主要介绍基于Jenkins实现持续集成的方式,通过案例介绍线上自动编译及部署的配置过程 持续集成 持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作 ...
- MyEclipse 编译错误 web项目中的 js,jsp报错 更改
搜索 validation 语法检测 必须 选定一个 不然不编译
- web 调用WCF 每次都要重新编译才能正常使用 终于解决了
项目中不知从何时起出现了个奇怪的问题,每次运行web都要全部重新编译解决方案,否则单独编译web,总是会出现WCF 调用失败的情况(什么无法激活服务,什么协议对地址不可用啊,七七八八的问题),因为这个 ...
- Jenkins结合.net平台之Web项目编译
前面我们讲解了如何使用msbuild.exe编译一个.net程序.示例中我们讲解的是编译控制台项目,但是我们知道web项目不仅需要编译类的嵌入的资源文件,还要拷贝诸如css,html,js,图片等资源 ...
- 来自后端的突袭? --浅尝最新开源的C# Web引擎 Blazor
在今年年初, 恰逢新春佳节临近的时候. 微软给全球的C#开发者们, 着实的送上了一分惊喜. 微软正式开源Blazor ,将.NET带回到浏览器. 这个小惊喜, 迅速的在dotnet开发者中间传开了. ...
- 来自后端的突袭? --开包即食的教程带你浅尝最新开源的C# Web引擎 Blazor
在今年年初, 恰逢新春佳节临近的时候. 微软给全球的C#开发者们, 着实的送上了一分惊喜. 微软正式开源Blazor ,将.NET带回到浏览器. 这个小惊喜, 迅速的在dotnet开发者中间传开了. ...
- 开包即食的教程带你浅尝最新开源的C# Web引擎Blazor
在今年年初,恰逢新春佳节临近的时候.微软给全球的C#开发者们,着实的送上了一分惊喜.微软正式开源Blazor,将.NET带回到浏览器. 这个小惊喜,迅速的在dotnet开发者中间传开了.201 ...
- WebAssembly,Web的新时代
在浏览器之争中,Chrome凭借JavaScript的卓越性能取得了市场主导地位,然而由于javascript的无类型特性,导致其运行时消耗大量的性能做为代价,这也是JavaScript的瓶颈之一.W ...
- 开包即食的教程带你浅尝最新开源的C# Web引擎 Blazor
在今年年初, 恰逢新春佳节临近的时候. 微软给全球的C#开发者们, 着实的送上了一分惊喜. 微软正式开源Blazor ,将.NET带回到浏览器. 这个小惊喜, 迅速的在dotnet开发者中间传开了. ...
- 项目自动化建构工具gradle 入门5——在intellij中做一个gradle的web工程
之前的几个小节,都是纯手工建文件夹,纯手工拷贝war包,或者纯手工解压个zip看看结果,,,,这还是我写了玩的helloWorld.若是玩大工程.几十个人的团队协同开发时,这么玩,,,,暴躁的程序员估 ...
随机推荐
- Django message组件
使用message组件要在seetings中配置 ①INSTALLED_APPS (项目需要什么功能都放在这 既可以有数据库,又可以写代码,html文件,和自己写的APP一个道理) ②MIDDLE ...
- 从浏览器架构认识BOM和DOM
浏览器架构 JavaScript运行在浏览器,BOM就是连接JavaScript代码和浏览器的桥梁,而DOM就是用来操作各种标签元素的. BOM包括 window.history.location.d ...
- Ehcache的Maven依赖及其配置文件
Ehcache的Maven依赖 <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> ...
- 基于Linux的三种防火墙(IPtables、Firewall、UFW)
学而不思则罔,思而不学则殆. 导航 IPtables Firewall UFW 对比总结 IPtables部分 1.IPtables 四表五链. 四表:filter.nat.raw.mangle. 五 ...
- 一文读懂LockSupport
阅读本文前,需要储备的知识点如下,点击链接直接跳转. java线程详解 Java不能操作内存?Unsafe了解一下 LockSupport介绍 搞java开发的基本都知道J.U.C并发包(即java. ...
- 【page cache】简介
目录 page cache 直接 IO 与 缓存 IO Linux IO 栈 Linux 中的具体实现 相关结构体 超级块 super_block 索引节点 inode 文件 file 目录项 den ...
- Mysql基础9-事务
一.事务简介 事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有操作作为一个整体一起向系统提交或者撤销操作请求,即这些操作要么同时成功,要么同时失败.mysql的事务默认是自动提交的,也就 ...
- Maven安装与配置教程
一.安装前检查 检查电脑上是否安装JDK,如果没有安装,请查看JDK安装教程:点我查看 如果电脑上已经安装JDK,按Win 和R键,输入cmd,然后点击确定 输入java -version,点击回车, ...
- 全景VR KRPano项目打包成安卓APP快速简易教程
有时候,我们可能不想把我们制作的全景VR项目发布到网站上,而是想把它作为一个手机应用来使用或者分享.这样,我们就可以更好地保护我们的作品,也可以更方便地展示给客户或者朋友.本文将介绍一种简单的方法,让 ...
- KRPANO PR10最新激活码(破解)分享
KRPano pr10最新版本激活码下载地址: http://pan.baidu.com/s/1qYv2vO4 适用于最新pr10以及之前版本,解压密码为KRPano技术解密群群号:551278936 ...