《WebAssembly 权威指南》(5)使用 C/C++ 和 WebAssembly

f8aa00660c642c86d870c84d20b7021e.gif

本文是《WebAssembly 权威指南》系列文章第 5 篇,系列文章列表:


这篇文章是《WebAssembly 权威指南》一书的第五章,介绍了如何使用 C/C++ 和 WebAssembly 开发应用程序。文章首先讲解了 C/C++ 语言和 WebAssembly 的兼容性和互操作性,以及一些常见的问题和解决方案。文章然后介绍了几种将 C/C++ 代码编译为 WebAssembly 模块的工具和方法,如 Emscripten、Clang 和 LLVM。文章还展示了如何使用 C/C++ 编写 WebAssembly 的函数、变量、内存、表和导入导出等特性。文章最后讲解了如何在浏览器中加载和运行 WebAssembly 模块,并使用 JavaScript 与之交互。

本书的转折点来了。到目前为止,我们一直专注于 WebAssembly 相关工具和技术栈。这是探索平台的方法有用,但是作为开发工具效率低下。高级编程语言早已使我们的专业超越了低级指令集的工作细节。用句法简洁、语义丰富的语言来表达逻辑,更容易、更有效率。

要真正体会到 WebAssembly 所提供的东西,我们需要考虑可编译为 WebAssembly 的源语言。问题是,并不是每个问题都能用 JavaScript 来表达,所以可以选择使用另一种语言,因为它的性能、表达的清晰性,或者仅仅是重复使用现有的代码,都是很有吸引力的。

C 语言是世界上最重要和最广泛使用的编程语言之一。我在高中时就开始在 Atari ST 电脑上使用它。我在《计算机语言》杂志上读到过它,一个朋友给了我一本开创性的、同名的书籍 《C 编程语言 [1]》作者是 Brian Kernighan 和已故伟大的 Dennis Ritchie。

有大量的 C 语言软件可以使用,其中大部分可以简单地重新编译成 WebAssembly。我们将在第 6 章 [2] 讨论现有库的移植问题。但现在我们将学习一点 C 语言,和使用它来改进我们迄今为止所尝试的一些工作。

使用 C 语言函数

C 语言函数在很多方面与 JavaScript 函数相似。它有自己的词法结构,并不附属于像类或结构那样的大单元。它可以接受也可以不接受参数。然而,它只能返回一个单一的值,并且不支持异常,所以错误处理往往比 C++、Java 或 JavaScript 更原始一些。

在例 5-1 中,有一个 C 语言实现了我们的年龄计算函数。这个程序很简单。这个例子甚至有一些基本的错误处理方法来处理参数错误的情况,即出生年份大于当前年份的情况。除非有来自未来的穿越者出现,否则这种情况不应该发生,我们应该处理它。更高级别的语言只是更容易来表达业务逻辑。

例 5-1. 一个简单的 C 语言程序

#include <stdio.h>

int howOld( int currentYear, int yearBorn )
{
    int retValue = -1;

    if ( yearBorn <= currentYear )
    {
        retValue = currentYear - yearBorn;
    }

    return(retValue);
}


int main()
{
    int age = howOld( 2021, 2000 );

    if ( age >= 0 )
    {
        printf( "You are % d!\n", age );
    } else { printf( "You haven't been born yet." ); }
}

不幸的是,计算机并不理解这些高级语言,所以我们需要将它们转换为二进制机器表示,以便执行。如果你只进行过 JavaScript 编程,这个过程可能略显陌生。作为一种解释性语言,你编写 JavaScript 并简单地运行它。就像所有的事情一样,都是有取舍的。对开发者来说很方便的东西,在运行时往往会明显变慢,而 C 和 C++ 在性能上长期以来一直占据着领先地位。

鉴于 C 语言的成熟度和对我们行业的重要性,有许多优秀的商业和开源的编译器。其中包括 GNU/Linux C 编译器(GCC)和 LLVM 的 Clang 编译器。我们将专注于后者,原因很快就会清楚。参考附录 [3] 来运行安装程序。即使在默认使用 Clang 的 macOS 上,如果没有安装 LLVM 的 WebAssembly 支持,也不是所有的命令都能开箱即用。

我们可以将 C 语言程序转换为可执行文件,如下所示:

brian@tweezer ~/g/w/s/ch05> clang howold.c 
brian@tweezer ~/g/w/s/ch05> ls -laF
total 112
drwxr-xr-x 4 brian staff   128 Feb 14 14:35 ./ 
drwxr-xr-x 6 brian staff   192 Feb 14 14:32 ../ 
-rwxr-xr-x 1 brian staff 49456 Feb 14 14:35 a.out* 
-rw-r--r-- 1 brian staff   343 Feb 14 14:36 howold.c

由于历史原因,生成的可执行文件被称为 a.out。你将在后面看到如何改变它。现在,我们可以执行该程序。

brian@tweezer ~/g/w/s/ch05> ./a.out
You are 21!

这是因为生成的可执行文件已经变成了 macOS 知道如何运行的合适格式。它是一个针对 64 位平台上的英特尔 x86 指令集的 Mach-O 可执行文件。

brian@tweezer ~/g/w/s/ch05> file a.out
a.out: Mach-O 64-bit executable x86_64

这个程序不能在 Windows 或 Linux 机器上运行。如果没有新的仿真层,它甚至不能在苹果新的基于 ARM 的机器上运行。这是因为 CPU 有一个指令集,涉及到将数值加载到寄存器中,在 CPU 上调用功能,并将结果存储在内存中。重新运行 clang 以支持 duce 汇编语言输出,而不是二进制可执行文件:

brian@tweezer ~/g/w/s/ch05> clang -S howold.c

产生的文件如例 5-2 所示。

例 5-2. 为我们的简单应用程序生成的汇编语言

.section __TEXT,__text,regular,pure_instructions
    .build_version macos, 13, 0 sdk_version 13, 1
    .globl _howOld                         ## -- Begin function howOld
    .p2align 4, 0x90
_howOld:                                ## @howOld
    .cfi_startproc
## % bb.0:
    pushq % rbp
    .cfi_def_cfa_offset 16
    .cfi_offset % rbp, -16
    movq % rsp, % rbp
    .cfi_def_cfa_register % rbp
    movl % edi, -4 (% rbp)
    movl % esi, -8 (% rbp)
    movl $-1, -12 (% rbp)
    movl -8 (% rbp), % eax
    cmpl -4 (% rbp), % eax
    jg LBB0_2
## % bb.1:
    movl -4 (% rbp), % eax
    subl -8 (% rbp), % eax
    movl % eax, -12 (% rbp)
LBB0_2:
    movl -12 (% rbp), % eax
    popq % rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .globl _main                           ## -- Begin function main
    .p2align 4, 0x90
_main:                                  ## @main
    .cfi_startproc
## % bb.0:
    pushq % rbp
    .cfi_def_cfa_offset 16
    .cfi_offset % rbp, -16
    movq % rsp, % rbp
    .cfi_def_cfa_register % rbp
    subq $16, % rsp
    movl $0, -4 (% rbp)
    movl $2021, % edi                     ## imm = 0x7E5
    movl $2000, % esi                     ## imm = 0x7D0
    callq _howOld
    movl % eax, -8 (% rbp)
    cmpl $0, -8 (% rbp)
    jl LBB1_2
## % bb.1:
    movl -8 (% rbp), % esi
    leaq L_.str (% rip), % rdi
    movb $0, % al
    callq _printf
    jmp LBB1_3
LBB1_2:
    leaq L_.str.1 (% rip), % rdi
    movb $0, % al
    callq _printf
LBB1_3:
    movl -4 (% rbp), % eax
    addq $16, % rsp
    popq % rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .section __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz "You are % d!\n"

L_.str.1:                               ## @.str.1
    .asciz "You haven't been born yet."

.subsections_via_symbols

正如你所看到的,它比我们的 C 程序要啰嗦得多。像函数调用、循环和条件检查这样的高级结构需要许多低级别的指令来表达。我们将需要一个实际的英特尔 x86 芯片来运行,或者至少是一个仿真的芯片。然而,在某种程度上,这与我们在前几章看到的 Wat 文件在概念上是相似的。

我们将 Clang 作为 C 编译器例子的主要原因是,它有一个基于 LLVM 项目的现代、可插拔的架构。在现代世界中,越来越多的竞争性指令集(如 x86、ARM、RISC-V)、新的编程语言(如 Rust、Julia、Swift),以及无论何种源语言都希望重复使用通用的优化,这一点是非常重要的。

在图 5-1 中,你可以看到这一过程包括三个环节。源代码被一个前端处理步骤解析。这将是特定于语言的。此步骤的输出是一个中间表示(IR),一个假设的但不是真实的机器的指令集。它以一种可以被优化层操作的格式捕获了所表达的逻辑。这个过程涉及到应用一个或多个转换 ,这些转换可以使代码更快、更有效。循环可以被展开。不使用的代码可能被删除,涉及常量的表达式可能被编译器评估,因此它们不需要在运行时评估,等等。最后一步是释放出一套本地的针对一个特定的运行时的指令。对于我们的目的,这显然是 Mach-O x86 64 位架构。

ed185897e4fe684bfcb67789ff0cc612.png

图 5-1. LLVM 的可插拔编译器架构

这些层中的任何一个都可以被替换成其他东西。正如我所提到的,Rust、Julia 和 Swift 等语言 ,都使用 LLVM 基础设施。这使语言作者不必每次都从头开始。他们需要编写新的前端解析器,但可以利用大部分现有的优化和后端工作。编译器研究人员可以开发新的优化,并在使其可用于任意输入语言的 IR 之前对其进行隔离测试。对于我们的目的,后端是最重要的可交换层。在 Linux 或 Windows 上,可以使用相同的前两层的本地版本,但也会有一个特定机器的后端。

你通常可以通过一个被称为交叉编译的过程,生成一个与你的计算机的本地运行时不同的后端。这对于针对可能没有安装开发者工具链的嵌入式系统很有用。这在持续集成和交付系统中也很有用,你可以从同一个构建环境中针对多个平台构建。否则,你可能需要为每个目标

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值