本文中,wasm=WASM=WebAssembly
刀与火的时代
在浏览器中渲染网页竟然是很困难的一件事情——很难想象这么简单的一件事情需要这么多奇技淫巧(浏览器从来都只负责渲染网页,前端本身却从来无法获得渲染后的结果):大名鼎鼎的html2canvas
已经做得很好了,但是有众多缺陷,并非完美复现;使用SVG元素的<foreignObject>
来插入外部元素并转化为图片是更新的方法,但是只能解析内联的样式;webkit.js
是大力出奇迹的典型:直接在浏览器里跑一个浏览器引擎,这样总行了吧...
借助 WebAssembly 的力量..
曾有一篇文章说:
webkit.js
的尝试在那个“pre wasm”的时代是一个非常有意义的尝试。
让我们在这个WASM时代更进一步——把浏览器引擎编译到WebAssembly,试图借助C++的力量,而不是用js重新实现一切。
思想实验
仅从“截图”这个需求来说,我们的沙盒浏览器只需要解析HTML和CSS就足够了,一切JS相关的内容由外部浏览器渲染完毕,我们只需要拿到完整的HTML内容和CSS并传给我们的沙盒浏览器。
抛弃JS引擎以后这个浏览器的门槛就大大降低,Github上的两个仓库引起了我的注意:Modest和litehtml。
Modest足够简单,pure c99,但渲染相关的内容并没有完成,wiki甚至是俄语写的...litehtml则相当好:pure c++并且依赖极少,编译到wasm的门槛很低。
按照litehtml的说法,这个引擎生成了渲染树后会调用几个document_container
下的渲染函数,而用户只需要实现这几个函数,使用自己喜欢的图形库来绘制结果。官方给出了几个现成的container:Linux下基于cairo的,Windows下基于HDC的。但我们最终要跑在一个特殊的环境下,所以这块看来需要自己完成了。
奇遇
撅腚了大致的技术路线,接下来需要解决的问题就是:
- 怎么在网页中呈现渲染结果?WASM官方提供了几个开箱即用的ports:SDL2、cocos2d-x、opengl。
- 怎么intergrate with
document_container
? - 怎么编译?
比较一番,SDL2应该算是最好的渲染方法了,官方也给了一个简单的例子:sdl-canvas-wasm。神奇的是,正好有个老哥在litehtml的issue里面上传了自己写的一个基于sdl的container,很不完善,但作为参考非常完美了(我回了一句Awesome!),遂决定在此基础上继续完善自己的渲染方法。
Compile the World
一下子前两个问题都解决了,在完善渲染方法之前,先编译到wasm下试一试能不能渲染出一些简单的内容吧。待测试的网页:
<div><h1>Hello world!</h1><p>created</p></div>
测试样式:
div{
position:fixed;
top:15px; left:0;
border:10px solid red;
width:300px;
height:400px;
}
测试函数:
int main(int argc, char const *argv[])
{
printf("start\n");
SDL_Init(SDL_INIT_VIDEO);
SDL_Renderer *renderer;
SDL_Window *window;
SDL_CreateWindowAndRenderer(1920, 1080, 0, &window, &renderer);
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderClear(renderer);
printf("create context\n");
litehtml::context ctx;
printf("load stylesheet\n");
ctx.load_master_stylesheet("div{position:fixed; top:15px; left:0; border:10px solid red; width:300px; height:400px;}");
printf("create document\n");
container_sdl doc(&ctx, renderer);
litehtml::document::ptr document = litehtml::document::createFromString("<div><h1>Hello world!</h1><p>created</p></div>", &doc, &ctx);
printf("ready to render\n");
document->render(1920);
printf("ready to draw\n");
litehtml::uint_ptr hdc;
const litehtml::position pos(0, 0, 1920, 1080);
document->draw(hdc, 0, 0, &pos);
return 0;
}
引用了litehtml的头文件和SDL的头文件,其中SDL是wasm-port,不需要实际安装环境。
编译命令:
emcc container_sdl.cpp -std=c++11 -s WASM=1 -s USE_SDL=2 -s USE_SDL_TTF=2 -O3 -o index.jsx.js
其中-s USE_SDL=2 -s USE_SDL_TTF=2
引入了SDL和SDL_ttf两个库,编译的时候emcc会自动下载、编译安装并加入环境。为了成功编译注释掉了一些代码,比如加载字体的时候不能访问本地文件,先注释掉,返回空指针。
太轻松了吧???
编译倒是成功了,然而链接的时候报找不到符号(函数):
这就是编译程序到 WebAssembly 的大坑之一:引用的库,也要由 emcc 编译一遍!
litehtml 这个库是在VS环境下写的,用VS编译出来是 .lib 格式的静态链接库,而 emscripten 只能链接由他的工具链编译出来的LLVM bitcode格式的库,这就意味着,我们要在Linux环境下编译一遍 litehtml。
CMake速成
既然要用GNU Make,需要先生成一份 Makefile,好在litehtml这个项目结构简单,重新写一份 CMakelist.txt 并不算太难...
cmake_minimum_required(VERSION 2.8)
project(litehtml)
# 启用c++11特性
SET(CMAKE_CXX_FLAGS "-std=c++0x")
# 加入源文件
set(SOURCE_LITEHTML
background.cpp
box.cpp
context.cpp
css_length.cpp
css_selector.cpp
document.cpp
el_anchor.cpp
el_base.cpp
el_before_after.cpp
el_body.cpp
el_break.cpp
el_cdata.cpp
el_comment.cpp
el_div.cpp
el_font.cpp
el_image.cpp
el_link.cpp
el_para.cpp
el_script
el_space.cpp
el_style.cpp
el_table.cpp
el_td.cpp
el_text.cpp
el_title.cpp
el_tr.cpp
element.cpp
html.cpp
html_tag.cpp
iterators.cpp
media_query.cpp
style.cpp
stylesheet.cpp
table.cpp
utf8_strings.cpp
web_color.cpp
)
add_library(${PROJECT_NAME} ${SOURCE_LITEHTML})
之后在litehtml的源码目录下跑一遍
emconfig emmake cmake . # 注意这有一个 .
emmake make
即可生成一个可以被emscripten链接的静态库:liblitehtml.a
如法炮制,把litehtml的依赖:gumbo也编译一下,得到libgumbo.a
。
把链接库文件也加入编译目标,最后编译就没问题了(大概需要一分钟):
运行结果:文字没有渲染出来是因为载入字体失败了(console),wasm下载入文件需要一些特殊方法,这些都有待完善。但div的边框仍然渲染出来了,算是阶段胜利吧!
后记
通过使用wasm内嵌文件的方式,能够成功载入字体。本次实验算是..圆满成功?