【填坑日志5】opencv.js生成 + LLVM框架

本文详细介绍了如何使用Emscripten编译OpenCV.js,以及LLVM编译框架的概念和作用,探讨了C++代码转化为JavaScript(WebAssembly)的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

第四篇已经讲到,我需要将一个jpg/bmp/png等图像文件格式转换为RGB/RGBA等图像通道的数据结构。这个工作可以借助专业的图形处理库opencv来处理。但是呢,opencv是基于C++语言的。近些年python和机器学习大火的情况下,opencv也跟着推出了python版本。opencv所有的功能基本上都是支持C++和python的。随着前端H5技术的发展,越来越多的图形处理在浏览器端实现。所以,opencv也开始推出了JS版本。但是JS版本支持的功能有限,不过对我自己的需求而言是已经足够了。
下面就是记录如何将C++的源码构建出JS文件来。主要涉及到下面几项内容,基本上也就是操作过程了:

  1. emscripten编译opencv.js
  2. LLVM编译架构
  3. webassembly
    Let‘s do it。

Emscripten编译opencv.js

安装Emscripten

IT行业里,不过接触什么新的技术,光学理论是不行的,一般来说都是先上手装一个。写出一个HelloWorld出来,就会有一个感性的认识,接下来再往深入了学习,就会快很多。那么就先装一个Emscripten编译器再说。
安装教程很多,我这里引用opencvjs教程里的指导:
安装Emscripten

如果你在执行
emsdk install latest命令一次性成功的,那么恭喜你,你的网速非常的惊人,或者是你的人品大爆发,反正我是从来没有成功过。
当然,这个网上也是有很多教程的,我是参考的这一份:
安装Emscripten-执行install
虽然是windows下的安装,但是道理是一样的,基本无影响。

安装完之后,记得在.bashrc文件中增加一个环境变量
在emsdk的安装目录下,找到emcc文件所在目录,把这个目录配置到EMSCRIPTEN的环境变量下:下面是我自己的配置地址:

export EMSCRIPTEN = /tools/emsdk/upstream/emscripten

再执行:

source ~/.bashrc

编译opencv.js

  1. 先去OpenCV的官网上下载一份最新的代码,这里我下载的是最新的4.3.0版本。我之前用3.4.2,在执行build_js命令的时候报错了,我猜应该是最新的emscripten的一些代码组织结构与opencv的不是很符合,所以才换成了最新的opencv版本,就一次性成功了。
  2. 进入opencv目录,执行python platform/js/build_js.py build_js,等上一会,就直接编译出来了,编译的结果存放在{opencv_dir}/build_js/bin里面,里面会有两个文件:opencv.js/opencv_js.js。我理解一个是压缩过的,一个是没压缩的,用起来是一样的。
    编译出来之后发现一个问题,这个opencv.js编译出来有7.8MB,这是要加载到浏览器中,网速不好的话都要下半天,我自己的应用是要针对手机提供的,7.8MB也够呛了,于是想想招看看是不是可以减少一点。
    opencv是分模块的,里面很多的关于3D图像的处理,目标跟踪、DNN分割的库我们是可以不用的,想着可以把这部分代码不进行编译和链接,是不是可以减少一点体积。
    找到{opencv_dir}/platform/js目录下的opencv_js.config.py。
    只留下core, imgproc, features2d三部分内容。
    最后的一行配置:
    white_list = makeWhiteList[]也只留下这三部分内容。
    然后重新执行
    python platform/js/build_js.py build_js
    结束了再一看:
    裁剪过的JS
    减少到了不到5MB。应该还可以针对这三个部分继续进行裁剪,没时间就暂时没管了。

使用opencv.js

这里碰到另外一个大坑,打出来的opencv.js无法使用import直接引入。我理解是opencv.js不支持ES6的模块化标准,里面没有直接为export属性赋值,所以没法直接import引入。这需要另外花一些时间来研究,而且我觉得这是一个巨大无比的坑。
好吧,最起码我已经把opencv.js已经编译出来了。这里就稍微偏一下题,看看LLVM到底是个啥。
这是因为,我的知识体系里,C++的代码编译成为目标文件(.o/.obj),这种文件是二进制的文件,最后再通过链接器链接成可执行文件(windows下的exe),这个LLVM架构是个啥玩意,就能把C++编译成JS。

LLVM编译框架

这个话题很大,这里我也只是对这个话题浅尝辄止。先了解下LLVM的作用、目标和结构,然后再综合这篇文章的主题,研究一下C++ => JS的这个过程在LLVM框架下是如何实现的。

LLVM是什么

一般来说,LLVM(Low Level Virtual Machine):底层虚拟机,可以有两层含义。
广义上的LLVM指的是整个的LLVM的框架,是一系列紧密联系的底层工具链组件的统称(例如链接、编译、调试等),同时包括一些 已有的、LLVM兼容的工具,是整个项目的一个统称。
以下信息摘自LLVM官网,简单翻译了一下。
LLVM的子项目有:

  1. LLVM Core 库。提供了一个现代的独立于源和目标的优化器,以及对许多流行 CPU(以及一些不太常见的CPU)的代码生成支持!这些库是围绕一个良好指定的代码表示构建的,该代码表示称为 LLVM 中间表示(“LLVM IR”)。LLVM Core 库有很好的文档说明,而且特别容易发明自己的语言(或移植现有编译器)来使用 LLVM 作为优化器和代码生成器。
  2. Clang编译器。Clang 是一个“LLVM 原生” C/C++/Objective-C 编译器,它的目标是提供惊人的快速编译(例如,在 debug 配置中编译 Objective-C 代码时,速度比 GCC 快 3 倍左右)、非常有用的错误和警告消息,并为构建优秀的源代码级工具提供平台。Clang Static Analyzer 是一种工具,可以自动发现代码中的bug,它是一个很好的例子,是一个使用 Clang frontend 作为库来解析 C/C++ 代码的工具。
  3. LLDB 项目建立在 LLVM 和 Clang 提供的库之上,以提供一个很棒的本机调试器。它使用了 Clang AST 和表达式解析器、LLVM JIT、LLVM 反汇编器等,从而提供了一种“正常工作”的体验。与 GDB 相比,它加载符号的速度更快,内存效率更高。
  4. libc++ 和 libc++ ABI 项目提供了 C++ 标准库的标准一致性和高性能实现,包括对 C++11 和 C++14 的完全支持。
  5. compiler-rt 项目提供了对底层代码生成器的高度调优实现,支持诸如 “__fixunsdfdi” 之类的例程,以及在目标没有用于实现核心 IR 操作的短序列本机指令时生成的其他调用。它还为动态测试工具(如 AddressSanitizer、ThreadSanitizer、MemorySanitizer 和 DataFlowSanitizer )提供了运行时库的实现。
  6. OpenMP 子项目为 Clang 中的 OpenMP 实现提供了一个 OpenMP 运行时。
  7. polly 项目使用多面体模型实现了一套缓存位置优化以及自动并行化和向量化。
  8. libclc 项目的目标是实现 OpenCL 标准库。
  9. klee 项目实现了一个“符号虚拟机”,它使用一个定理验证器来评估程序中的所有动态路径,以发现 bug 并证明函数的属性。klee 的一个主要特性是,它可以在检测到 bug 时生成一个测试用例。
  10. SAFECode 项目是一个用于 C/C++ 程序的内存安全编译器。它使用运行时检查来检测运行时的内存安全错误(例如,缓冲区溢出)。它可以用来保护软件免受安全攻击,也可以像 Valgrind 一样用作内存安全错误调试工具。
  11. LLD 项目是一个新的链接器。这是一个系统链接器的临时替代品,运行速度更快。
    除了LLVM的官方子项目之外,还有许多其他项目使用 LLVM 的组件来完成各种任务。通过这些外部项目,您可以使用 LLVM 编译 Ruby、Python、Haskell、Java、D、PHP、Pure、Lua 和其他一些语言。LLVM的一个主要优点是它的通用性、灵活性和可重用性,这就是为什么它被用于如此广泛的不同任务:从轻量级 JIT 编译嵌入式语言(如 Lua)到为大型超级计算机编译 Fortran 代码。

而一般我们说的LLVM就是指的Clang + LLVM core优化器

为什么要有LLVM,能做什么?

编译器(像大多数 C 编译器)的设计就是「三段式的设计」,即前端、优化器和后端(下图)。前端解析源码,检查错误,然后编译成指定语言的抽象语法树(Abstract Syntax Tree)。可以选择性地将 AST 转换为新的中间码以进行优化,优化器和后端生成机器码。
三段式编译器
优化器负责做各种转化尝试提高代码的执行效率,如消除冗余的计算,优化器通常或多或少地独立于语言和目标机器。后端(也叫代码生成器)负责将代码映射到目标指令集。除了生成正确的代码,它还负责利用目标架构的特殊功能生成性能更好的代码。在使用GCC或者JAVA -c等编译命令的时候,使用的各种编译参数,我理解就是让优化器使用不同的优化策略去优化。

编译器后端的通用功能包括指令选择、寄存器分配和指令调度。

C++的编译过程

我们使用gcc工具来看一下,这样一个三段式过程中,每个过程间的接口是什么样的一个东西。
首先,我们可以看一下gcc的编译/链接过程:
gcc工作过程
我们写一个简单的C++程序:

#include “stdio.h”
int main()
{
    printf("Hello World");
    return 0;
}
预处理

gcc -E只是执行预处理,不进行真实的编译,汇编和链接操作。可以执行gcc --help看一下说明。
调用gcc -E hello.cpp -o hello.i
成功后,生成了一个hello.i的文件,这个文件很长,有好几百行,远比我们写的代码多。主要干了几件事情:

  1. 把注释去掉
  2. 处理引用的头文件,看代码像是把和这个头文件相关的所有文件引入进来。没仔细研究。
  3. 把宏定义替换成可执行的代码
    总的说来,预处理之后的文件还是一个C语言的文件,只是把一些语法糖,注释等与执行关系不大的东西换成更纯粹的代码。
转换成汇编

执行:

gcc -S hello.cpp -o hello.s

成功后打开hello.i查看,就是一份汇编代码:
汇编代码
大学的时候学过,基本上也就是一些操作寄存器之类的东西,现在基本也看不懂了。

转换二进制目标文件和可执行文件
gcc -c hello.cpp -o hello.o

执行这条命令后生成目标二进制文件。再通过:

gcc helloc.cpp hello

把相关的静态库、动态库链接到一起,形成可执行文件。

那么,我们来看一下上面的几个步骤,我理解在三段式的编译器涉及里,
前端部分就是gcc -E的预处理部分和gcc -S形成汇编语言为前端
而优化器指的是汇编语言到目标文件的过程。
后端部分指的是目标文件+链接过程,这部分与目标操作系统强相关(指令集等)。
应该说这个没法生搬硬套,但可以借助这么个过程来理解(优化器应该是也应用到了前端的gcc -E和 gcc -S部分)。

按照这个三段式的涉及,使编译器支持一种新的语言需要实现一个新的前端,但是优化器以及后端都是可以复用,不用改变的。那么实现支持新的语言需要从最初的前端设计开始,支持N种设备和M种源代码语言一共需要N*M种编译方式。

JAVA

java语言利用了虚拟机来屏蔽C语言编译器后端对操作系统的依赖,利用javac工具对代码进行预处理、编译等操作,形成Java字节码:.class文件。而java虚拟机可以将字节码读入后执行(类似与执行C编译器的优化器与后端的操作部分)

LLVM都有什么

LLVM更进一步,将三个部分完全拆开来看,定义了中间描述语言:LLVM IR(Intermedia Representation)
LLVM整体结构
也就是说,LLVM定义了一个标准:大家的各种语言都按照这个标准来,每个编译器的前端通过各种操作,最后转成IR,优化器也基于IR来做优化,后端转成各种操作系统的也是基于IR去转。
定义这个标准之后,广义的LLVM就做了一堆前面讲的内容。

C++到JS – WebAssembly

这里在网上看到了一篇文章,引用过来,讲的比较清楚:
LLVM + WebAssembly
文章的前半部分也是讲的LLVM,逻辑上就是我前面讲的,但是作者画了图,更好的去理解这个过程。
C++和JS我认为是完全两个世界的语言,一个是静态强类型语言,一个是弱类型动态语言。在我们这个主题下,两门语言也完全不同:
C++:编译型语言。
JS:解释型语言。
关于这两个概念,上面引入的这篇文章也讲的比较清楚了,就是不同的翻译方式。
而WebAssembly想的就是综合两种方式的优点。我自己的理解:WebAssembly包括下面两点内容:

  1. 一种标准或规范。在LLVM架构里,使用Clang编译器讲C++编译成了LLVM的中间格式–IR。然后再使用Emscripten这个后端,讲IR转成目标系统(浏览器虚拟运行环境)的格式–WASM。浏览器的玩家想要使用这种高级货,就必须支持这种标准或者规范。
  2. 比JIT更高效的JS解释器和运行环境。编译优化等工作已经在JS->WASM的过程中解决了大部分,从而减少了客户端的压力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

新兴AI民工

码字不易,各位看客随意

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值