本文中,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会自动下载、编译安装并加入环境。为了成功编译注释掉了一些代码,比如加载字体的时候不能访问本地文件,先注释掉,返回空指针。

太轻松了吧???

编译倒是成功了,然而链接的时候报找不到符号(函数):
unsolved symbol
这就是编译程序到 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

把链接库文件也加入编译目标,最后编译就没问题了(大概需要一分钟):
图片.png

运行结果:文字没有渲染出来是因为载入字体失败了(console),wasm下载入文件需要一些特殊方法,这些都有待完善。但div的边框仍然渲染出来了,算是阶段胜利吧!
图片.png

后记

通过使用wasm内嵌文件的方式,能够成功载入字体。本次实验算是..圆满成功?