65.9K
CodeProject 正在变化。 阅读更多。
Home

将你的 C++ 代码带到 Web

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (10投票s)

2019年6月29日

CPOL

8分钟阅读

viewsIcon

16575

downloadIcon

254

如何将您的 C++ 代码带到 Web

目录

引言

WebAssembly 的前身 asm.js 将 C/C++、Rust 代码转换为低级 JavaScript,以便在 Web 浏览器上运行。WebAssembly 于 2017 年 3 月推出,它使代码(无论是原生代码还是托管代码)都能编译成基于堆栈的虚拟机二进制指令格式,从而在 JavaScript 的性能上实现了 20% 到 600% 的提升,这是十年前无法想象的壮举。今天的文章将提供分步说明,介绍如何安装 Emscripten 工具集,并在 20 分钟内让“Hello World”程序在我们选择的 Web 浏览器上运行。读者的实际操作时间可能会因其互联网速度/质量而异。

安装适用于 Linux 的 Windows 子系统

为了使用 Emscripten,我们需要安装适用于 Linux 的 Windows 子系统 (WSL),这是 Windows 10 上的一项功能,因此不支持旧版本的 Windows。如果您是 apt Linux 用户,欢迎使用您喜欢的 Linux 发行版。如果您不想安装 Linux 操作系统,也可以下载 Emscripten Windows 安装程序。这些 Windows 安装程序在我几年前使用时就没起作用,但情况可能已经有所改善。在尝试 Linux 路线之前,尝试 Windows 安装程序没有坏处。最新的 Windows 安装程序通常比最新版本落后几个版本。如果您不是那些紧跟技术前沿的人,这也许是可以的。

为什么选择 WSL?

其他虚拟机选项包括 Oracle 的 VirtualBox 和 Microsoft 的 Hyper-V。VirtualBox 只支持 32 位客户机操作系统。Emscripten 工具需要 4GB 以上的 RAM 才能构建,更准确地说,是为了链接而不是编译。超过 4GB 的 RAM 意味着 64 位操作系统,因此排除了 VirtualBox。Hyper-V 支持 64 位客户机,但仅随 Windows 10 Pro 提供。家庭用户通常拥有 Windows 家庭版。在我们的情况下,WSL 是最吸引人的选择。

在 Windows 10 上启用 WSL

在我们可以从 Microsoft 安装 Ubuntu 之前,我们必须先启用开发人员模式和 WSL。要启用开发人员模式,请转到“设置”>“更新和安全”>“开发者选项”,然后选择“开发人员模式”。

也可以通过 PowerShell 完成此操作。以管理员模式启动 PowerShell。然后键入以下命令并按“Enter”键。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

接下来,必须在“Windows 功能”中启用 WSL。在 Windows 搜索栏中,键入“打开或关闭 Windows 功能”并选择该选项。一直向下滚动并选中“适用于 Linux 的 Windows 子系统”选项,如图所示。

安装 Ubuntu 18.04

接下来,通过单击任务栏上的按钮启动 Microsoft Store。搜索“Ubuntu”,然后在搜索结果中单击 Ubuntu 18.04 上的“安装”。这大约需要 5 分钟。

Ubuntu 首次启动重要提示

Ubuntu 安装完成后,Windows 会要求您启动它。停止!不要按“启动”按钮!您必须通过右键单击 Ubuntu 并选择“以管理员身份运行”来以管理员权限启动它。在此消息“正在安装,这可能需要几分钟……”出现然后消失后,Ubuntu 会在第一次启动时提示您输入用户名和密码。现在我们可以开始安装 Emscripten 了。

安装 Emscripten

更新 Ubuntu 包并安装 Python 2.7

由于这是全新的 Ubuntu 安装,在安装 Python 之前我们需要更新包。运行这 3 条命令。这可能需要相当长的时间,但请勿中途取消。

sudo apt update
sudo apt upgrade
sudo apt install python2.7 python-pip

您可能需要安装 Git。运行以下命令

sudo apt-get install git-core

接下来,从 GitHub 获取 Emscripten。

git clone https://github.com/emscripten-core/emsdk.git

最后,我们准备好安装 Emscripten 了。运行以下命令下载并安装预编译的工具链。

# change to the newly cloned emsdk directory.
cd emsdk

# Download and install the latest SDK tools.
./emsdk install latest

# Set up the compiler configuration to point to the "latest" SDK.
./emsdk activate latest

# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh

最后一步,为了在您使用 Emscripten 工具集之前设置环境变量和路径,必须在每次 Ubuntu 启动时调用它。

为不受支持的 Linux 发行版或只是为了好玩而编译工具链

对于那些使用不受支持的 Linux 发行版的读者,您可以使用这些命令构建 Emscripten 工具链。根据您的存储类型(如 SSD 或 HDD)、CPU 核心数量和 RAM 容量,构建过程最多可能需要 3 小时。

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --build=Release sdk-incoming-64bit binaryen-master-64bit

Hello World 程序

在本节中,我们将构建一个 C“hello world”程序,然后是 C++ 版本,并比较它们的输出文件大小。

Hello World C 程序

这是我们使用的 C 源代码。这个程序有一个我必须告诉您的怪癖。printf() 格式字符串必须以换行符结尾,它起到刷新控制台输出的标志作用,否则您将得不到任何输出。

#include <stdio.h>

int main() 
{
  printf("Hello, world!\n");
}

我们将源代码保存在 hello.c 中。这是构建 hello.c 的命令。它大约需要 5 分钟,因为它会构建所需的 C 静态库,文件扩展名为 bc。后续构建应该会很快。

emcc hello.c -s WASM=1 -o hello.html

编译的输出是 hello.htmlhello.js 以及最后的 hello.wasm,它是 Webassembly 文件。

Hello World C++ 程序

#include <iostream>

int main() 
{
  std::cout << "Hello, world!" << std::endl;
}

我们将 C++ 源代码保存在 hello2.cpp 中。这是构建 hello2.cpp 的命令。与 C 版本一样,构建 C++ 静态库需要一些时间。

emcc hello2.cpp -s WASM=1 -o hello2.html

编译的输出是 hello2.htmlhello2.js 以及最后的 hello2.wasm。单个文件编译对于一个文件来说是可以的。要使用 make 命令构建多个源文件,只需用文本编辑器打开现有的 Makefile,并将所有“gcc”或“g++”的出现替换为“emcc”。C++ 输出文件比 C 文件大 400KB,这是因为仅仅包含 iostream 头文件就引入了模板膨胀。

要查看 HTML,请打开 Visual Studio 2019 并创建一个 ASP.NET 项目,然后将刚才提到的 HTML、JavaScript 和 wasm 文件添加到新创建的项目中。右键单击 hello.html 在浏览器中查看。对 hello2.html 执行相同的操作。Visual Studio 2017 的开发 Web 服务器在提供 wasm 文件方面存在问题。请使用 VS2019 或其他 Web 服务器。对于其他 Web 服务器,您可能需要在其配置文件中为 Webassembly 添加 MIME 类型(Content-Type=application/wasm),如果尚未添加的话。执行此操作的确切方法因每个 Web 服务器而异,请务必查阅手册或说明指南。如果您不想设置 MIME 类型并想查看 HTML 输出,请尝试在 emcc 构建期间将 WASM=0 设置为生成 asm.js 代码。asm.js 文件类型是所有 Web 服务器都能轻松提供的合法 JavaScript 文件。asm.js 处于维护模式,这意味着所有新颖的功能都将只添加到 Webassembly。在接下来的 2 个部分中,我们将探讨 C/C++ 与 JavaScript 的交互。

从 C/C++ 调用 JavaScript 代码

要从 C++ 调用 JavaScript,请将您的 JavaScript 代码放在 EM_ASM() 中。由于 EM_ASM() 不是合法的 C++ 代码,因此您必须用宏将其保护起来,以阻止 C++ 编译器解析它。在我的情况下,我在 Emscripten Makefile 中声明了 __EMSCRIPTEN__。下面的示例查找一个名为 MyMusicWebAudio 元素并调用其 play 方法来播放 MP3。

#ifdef __EMSCRIPTEN__
EM_ASM(
    document.getElementById("MyMusic").play(); 
);
#endif

要将一些参数传递给 EM_ASM JavaScript 片段,请调用带有下划线后缀的 EM_ASM_ 和您的参数:$0 是第一个参数的占位符,$1 是第二个,依此类推。

EM_ASM_({
    console.log('I received: ' + $0);
}, 100);

要从 JavaScript 代码片段返回整数或双精度值,请调用 EM_ASM_INTEM_ASM_DOUBLE

int x = EM_ASM_INT({
    console.log('I received: ' + $0);
    return $0 + 1;
}, 100);
printf("%d\n", x);

从 JavaScript 调用 C/C++ 代码

为了让 JavaScript 调用 C 函数,您必须在编译期间导出 C 函数,然后在 JavaScript 中调用 Module.cwrap() 将其放入一个可调用的包装器中。下面是 gen_enum_conv 的签名。由于所有 C++ 编译器都会进行名称修饰/更改:为了避免这种情况,使函数名保持不变以便 JavaScript 找到它,我们必须在函数签名之前声明 extern "C"

extern "C" const char* gen_enum_conv(const char* cs);

这是具有 maingen_enum_conv 导出函数的 Makefile:这两个名称都以在命名约定中很常见的下划线开头。

CC=emcc
SOURCES:=~/EnumStrConv.cpp
SOURCES+=~/ParseEnum.cpp
LDFLAGS=-O2 --llvm-opts 2
OUTPUT=~/EnumConvGen.html
EMCC_DEBUG=1

all: $(SOURCES) $(OUTPUT)

$(OUTPUT): $(SOURCES) 
	$(CC) $(SOURCES) --bind -s NO_EXIT_RUNTIME=1 -s 
    EXPORTED_FUNCTIONS="['_main', '_gen_enum_conv']" -s 
    ALLOW_MEMORY_GROWTH=1 -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=1 
                      -D__EMSCRIPTEN__ -std=c++11 $(LDFLAGS) -o $(OUTPUT)

clean:
	rm $(OUTPUT)

下面是用于包装此函数并由按钮单击调用的 JavaScript 代码。要深入了解更多代码,请访问 EnumConvGen GitHubEnumConvGen 是一个 C++ 项目,用于生成 C++ enumstring 的转换函数,反之亦然。

<script>
var gen_enum_conv_func;
function btnClick()
{
    var input_str = document.getElementById("InputTextArea").value;
    document.getElementById("OutputTextArea").value = gen_enum_conv_func(input_str);
}
$( document ).ready(function() {
    gen_enum_conv_func = Module.cwrap('gen_enum_conv', 'string', ['string']);
    $('[name="GenButton"]').click(btnClick);
});
</script>

调用 C++ 成员函数呢?

我刚才向您展示的是如何调用 C 函数。那么如何从 JavaScript 调用 C++ 成员函数呢?您必须使用 embind 来完成此操作。有关如何操作,请参阅其 文档。我个人不使用 embind,因为它很复杂。我通常的做法是将所有 C++ 调用都封装在 C 函数体中。我还没有遇到过需要我专门使用 embind 来完成所需任务的情况。

跨平台

Emscripten 仅限于调用 C++ 标准库和 emscripten 移植的 C/C++ 库(见下文列表)的可移植函数。特定于操作系统的函数(如 win32)和包含汇编代码的函数是不可能的。库列表主要集中在图形、音频、网络和字体领域,正如您所见,这些都是为了支持游戏编程而设计的。

  • SDL2
  • regal
  • HarfBuzz
  • SDL2_mixer
  • SDL2_image
  • Cocos2d
  • FreeType
  • asio
  • SDL2_net
  • SDL2_ttf
  • Vorbis
  • Ogg
  • Bullet
  • libpng
  • zlib

好了,各位!请继续关注“带上您的 XXX”系列文章的第二部分!与此同时,尽情享受 Webassembly 的乐趣吧!

《将你的...》系列的其他文章

历史

  • 2019年6月29日:初稿
© . All rights reserved.