什么? C 语言动态库免费大放送了?

开发 前端
今天给大家看一个魔法, 这个魔法可以让你非常方便的在 luajit 里面使用高性能的 C/CPP 库, 从而避免自己造轮子的痛苦.

看到有同学说 Lua 库少, 需要自己造轮子. 其实不是这样的, 今天给大家看一个魔法, 这个魔法可以让你非常方便的在 luajit 里面使用高性能的 C/CPP 库, 从而避免自己造轮子的痛苦.

[[360633]]

这个魔法是 FFI ( Foreign function interface ), 我并不打算仔细讲 FFI 原理, 所以简单来说, FFI 实现了跨语言的二进制接口. 它的优点是高效方便. 直接调用 ABI, 缺点也很明显, 出了问题直接会挂掉, 因此数据跨临界区前仔细检查就可以了.

我们今天直接找个 C 语言库, 然后利用 FFI 在 luajit 里面调用这个函数库作为个大家的演示.

什么? 这里竟然躺着一个高性能 base64 库?

我们以这个 repo 为例: https:// github.com/aklomp/base6 4 . 这是一个 C 编写的 Base64 编码/解码库, 而且支持SIMD.

可以简单运行下这个库的 benchmark:

  1. karminski@router02:/data/works/base64$ make clean && SSSE3_CFLAGS=-mssse3 AVX2_CFLAGS=-mavx2 make && make -C test 
  2. ... 
  3. Testing with buffer size 100 KB, fastest of 10 * 100 
  4. AVX2    encode  12718.47 MB/sec 
  5. AVX2    decode  14542.81 MB/sec 
  6. plain   encode  3657.40 MB/sec 
  7. plain   decode  3433.23 MB/sec 
  8. SSSE3   encode  7269.55 MB/sec 
  9. SSSE3   decode  8173.10 MB/sec 
  10. ... 

我的 CPU 是 Intel(R) Xeon(R) CPU E3-1246 v3 @ 3.50GHz, 可以看到CPU如果支持 AVX2 的话, 可以达到 12GB/s 以上, 这个性能非常强悍, 甚至连普通的SSD都跟不上了.

我们需要的第一步是把这个 repo 编译为动态库. 但是这个 repo 并没有提供动态库的编译选项, 所以我们魔改下这个项目的 Makefile.

  1. CFLAGS += -std=c99 -O3 -Wall -Wextra -pedantic 
  2.  
  3. # Set OBJCOPY if not defined by environment: 
  4. OBJCOPY ?= objcopy 
  5.  
  6. OBJS = \ 
  7.   lib/arch/avx2/codec.o \ 
  8.   lib/arch/generic/codec.o \ 
  9.   lib/arch/neon32/codec.o \ 
  10.   lib/arch/neon64/codec.o \ 
  11.   lib/arch/ssse3/codec.o \ 
  12.   lib/arch/sse41/codec.o \ 
  13.   lib/arch/sse42/codec.o \ 
  14.   lib/arch/avx/codec.o \ 
  15.   lib/lib.o \ 
  16.   lib/codec_choose.o \ 
  17.   lib/tables/tables.o 
  18.  
  19. SOOBJS = \ 
  20.   lib/arch/avx2/codec.so \ 
  21.   lib/arch/generic/codec.so \ 
  22.   lib/arch/neon32/codec.so \ 
  23.   lib/arch/neon64/codec.so \ 
  24.   lib/arch/ssse3/codec.so \ 
  25.   lib/arch/sse41/codec.so \ 
  26.   lib/arch/sse42/codec.so \ 
  27.   lib/arch/avx/codec.so \ 
  28.   lib/lib.so \ 
  29.   lib/codec_choose.so \ 
  30.   lib/tables/tables.so 
  31.  
  32. HAVE_AVX2   = 0 
  33. HAVE_NEON32 = 0 
  34. HAVE_NEON64 = 0 
  35. HAVE_SSSE3  = 0 
  36. HAVE_SSE41  = 0 
  37. HAVE_SSE42  = 0 
  38. HAVE_AVX    = 0 
  39.  
  40. # The user should supply compiler flags for the codecs they want to build. 
  41. # Check which codecs we're going to include: 
  42. ifdef AVX2_CFLAGS 
  43.   HAVE_AVX2 = 1 
  44. endif 
  45. ifdef NEON32_CFLAGS 
  46.   HAVE_NEON32 = 1 
  47. endif 
  48. ifdef NEON64_CFLAGS 
  49.   HAVE_NEON64 = 1 
  50. endif 
  51. ifdef SSSE3_CFLAGS 
  52.   HAVE_SSSE3 = 1 
  53. endif 
  54. ifdef SSE41_CFLAGS 
  55.   HAVE_SSE41 = 1 
  56. endif 
  57. ifdef SSE42_CFLAGS 
  58.   HAVE_SSE42 = 1 
  59. endif 
  60. ifdef AVX_CFLAGS 
  61.   HAVE_AVX = 1 
  62. endif 
  63. ifdef OPENMP 
  64.   CFLAGS += -fopenmp 
  65. endif 
  66.  
  67.  
  68. .PHONY: all analyze clean 
  69.  
  70. all: bin/base64 lib/libbase64.o lib/libbase64.so 
  71.  
  72. bin/base64: bin/base64.o lib/libbase64.o lib/libbase64.so 
  73.     $(CC) $(CFLAGS) -o $@ $^ 
  74.  
  75. lib/libbase64.o: $(OBJS) 
  76.     $(LD) -r -o $@ $^ 
  77.     $(OBJCOPY) --keep-global-symbols=lib/exports.txt $@ 
  78.  
  79. lib/libbase64.so: $(SOOBJS) 
  80.     $(LD) -shared -fPIC -o $@ $^ 
  81.     $(OBJCOPY) --keep-global-symbols=lib/exports.txt $@ 
  82.  
  83. lib/config.h: 
  84.     @echo "#define HAVE_AVX2   $(HAVE_AVX2)"    > $@ 
  85.     @echo "#define HAVE_NEON32 $(HAVE_NEON32)" >> $@ 
  86.     @echo "#define HAVE_NEON64 $(HAVE_NEON64)" >> $@ 
  87.     @echo "#define HAVE_SSSE3  $(HAVE_SSSE3)"  >> $@ 
  88.     @echo "#define HAVE_SSE41  $(HAVE_SSE41)"  >> $@ 
  89.     @echo "#define HAVE_SSE42  $(HAVE_SSE42)"  >> $@ 
  90.     @echo "#define HAVE_AVX    $(HAVE_AVX)"    >> $@ 
  91.  
  92. $(OBJS): lib/config.h 
  93.  
  94. $(SOOBJS): lib/config.h 
  95.  
  96. # o 
  97. lib/arch/avx2/codec.o:   CFLAGS += $(AVX2_CFLAGS) 
  98. lib/arch/neon32/codec.o: CFLAGS += $(NEON32_CFLAGS) 
  99. lib/arch/neon64/codec.o: CFLAGS += $(NEON64_CFLAGS) 
  100. lib/arch/ssse3/codec.o:  CFLAGS += $(SSSE3_CFLAGS) 
  101. lib/arch/sse41/codec.o:  CFLAGS += $(SSE41_CFLAGS) 
  102. lib/arch/sse42/codec.o:  CFLAGS += $(SSE42_CFLAGS) 
  103. lib/arch/avx/codec.o:    CFLAGS += $(AVX_CFLAGS) 
  104. # so 
  105. lib/arch/avx2/codec.so:   CFLAGS += $(AVX2_CFLAGS) 
  106. lib/arch/neon32/codec.so: CFLAGS += $(NEON32_CFLAGS) 
  107. lib/arch/neon64/codec.so: CFLAGS += $(NEON64_CFLAGS) 
  108. lib/arch/ssse3/codec.so:  CFLAGS += $(SSSE3_CFLAGS) 
  109. lib/arch/sse41/codec.so:  CFLAGS += $(SSE41_CFLAGS) 
  110. lib/arch/sse42/codec.so:  CFLAGS += $(SSE42_CFLAGS) 
  111. lib/arch/avx/codec.so:    CFLAGS += $(AVX_CFLAGS) 
  112.  
  113. %.o: %.c 
  114.     $(CC) $(CFLAGS) -o $@ -c $< 
  115.  
  116. %.so: %.c 
  117.     $(CC) $(CFLAGS) -shared -fPIC -o $@ -c $< 
  118.  
  119. analyze: clean 
  120.     scan-build --use-analyzer=`which clang` --status-bugs make 
  121.  
  122. clean: 
  123.     rm -f bin/base64 bin/base64.o lib/libbase64.o lib/libbase64.so lib/config.h $(OBJS) 

看不懂没关系, Makefile 是如此的复杂, 我也看不懂, 仅仅是凭着感觉修改的, 然后他就恰好能运行了... 注意 Makefile 的缩进一定要用 "\t", 否则不符合语法会报错.

然后我们进行编译:

  1. AVX2_CFLAGS=-mavx2 SSSE3_CFLAGS=-mssse3 SSE41_CFLAGS=-msse4.1 SSE42_CFLAGS=-msse4.2 AVX_CFLAGS=-mavx make lib/libbase64.so 

这样我们就得到了libbase64.so 动态库 (在 lib 里面). 这里还顺便开启了各种 SIMD 选项. 如果不需要的话可以关闭.

魔改开始

当然这只是魔法, 不是炼金术, 所以是需要付出努力的, 我们要手动实现动态库的桥接, 首先我们需要查看我们要调用的函数需要什么参数. 这两个定义很简单, 我们需要传入:

  1. const char *src 
  2. size_t srclen 
  3. char *out 
  4. size_t *outlen 
  5. int flags 
  1. void base64_encode(const char *src, size_t srclen, char *out, size_t *outlen, int flags); 
  2. int  base64_decode(const char *src, size_t srclen, char *out, size_t *outlen, int flags); 

然后我们就可以开始编写 ffi 桥接程序了. 首先把需要的库全都包含进来, 注意, 多用 local 没坏处, 使用 local 可以有效从局部查询, 避免低效的全局查询. 甚至其他包中的函数都可以 local 一下来提升性能.

动态库的话用专用的 ffi.load 来引用.

然后定义一个 _M 用来包裹我们的库. 这里跟 JavaScript 很像, JavaScript 在浏览器里有 window, Lua 有 _G. 我们要尽可能避免封装好的库直接扔给全局, 因此封装起来是个好办法.

  1. -- init 
  2. local ffi        = require "ffi" 
  3. local floor      = math.floor 
  4. local ffi_new    = ffi.new 
  5. local ffi_str    = ffi.string 
  6. local ffi_typeof = ffi.typeof 
  7. local C          = ffi.C 
  8. local libbase64  = ffi.load("./libbase64.so") -- change this path when needed. 
  9.  
  10. local _M = { _VERSION = '0.0.1' } 

然后是用 ffi.cdef 声明 ABI 接口, 这里更简单, 直接把源代码的头文件中的函数声明拷过来就完事了:

  1. -- cdef 
  2. ffi.cdef[[ 
  3. void base64_encode(const uint8_t *src, size_t srclen, uint8_t *out, size_t *outlen, size_t flags); 
  4. int  base64_decode(const uint8_t *src, size_t srclen, uint8_t *out, size_t *outlen, size_t flags); 
  5. ]] 

接下来是最重要的类型转换:

  1. -- define types 
  2. local uint8t    = ffi_typeof("uint8_t[?]") -- uint8_t * 
  3. local psizet    = ffi_typeof("size_t[1]")  -- size_t * 
  4.  
  5. -- package function 
  6. function _M.base64_encode(src, flags) 
  7.     local dlen   = floor((#src * 8 + 4) / 6
  8.     local out    = ffi_new(uint8t, dlen) 
  9.     local outlen = ffi_new(psizet, 1
  10.     libbase64.base64_encode(src, #src, out, outlen, flags) 
  11.     return ffi_str(out, outlen[0]) 
  12.  
  13. end  
  14.  
  15. function _M.base64_decode(src, flags) 
  16.     local dlen   = floor((#src + 1) * 6 / 8
  17.     local out    = ffi_new(uint8t, dlen) 
  18.     local outlen = ffi_new(psizet, 1
  19.     libbase64.base64_decode(src, #src, out, outlen, flags) 
  20.     return ffi_str(out, outlen[0]) 
  21. end 

我们用 ffi_typeof 来定义需要映射的数据类型, 然后用 ffi_new 来将其实例化, 分配内存空间. 具体来讲:

我们定义了2种数据类型, 其中, local uint8t = ffi_typeof("uint8_t[?]") 类型用来传输字符串, 后面的问号是给 local out = ffi_new(uint8t, dlen) 中的 ffi_new 函数准备的, 它的第二个参数可以指定实例化该数据类型时的长度. 这样我们就得到了一个空的字符串数组, 用来装 C 函数返回的结果. 这里的 dlen 计算出了源字符串 base64 encode 之后的长度, 分配该长度即可.

同样, local psizet = ffi_typeof("size_t[1]") 指定了一个 size_t * 类型. C 语言里面数组就是指针, 即 size_t[0] 与 site_t* 是等价的. 因此我们分只有一个元素的 size_t 数组就得到了指向 size_t 类型的指针. 然后在 local outlen = ffi_new(psizet, 1) 的时候后面的参数写的也是1, 不过这里写什么已经无所谓了, 它只是不支持传进去空, 所以我们相当于传了个 placeholder.

在使用这个值的时候, 我们也是按照数组的模式去使用的: return ffi_str(out, outlen[0]) .

需要注意的是, 一定要将 require "ffi" 以及 ffi.load 放在代码最底层, 否则会出现 table overflow 的情况.

最后, 这个文件是这样子的:

  1. --[[ 
  2.   
  3.     ffi-base64.lua 
  4.      
  5.     @version    20201228:1 
  6.     @author     karminski <code.karminski@outlook.com> 
  7.  
  8. ]]-- 
  9.  
  10. -- init 
  11. local ffi        = require "ffi" 
  12. local floor      = math.floor 
  13. local ffi_new    = ffi.new 
  14. local ffi_str    = ffi.string 
  15. local ffi_typeof = ffi.typeof 
  16. local C          = ffi.C 
  17. local libbase64  = ffi.load("./libbase64.so") -- change this path when needed. 
  18.  
  19. local _M = { _VERSION = '0.0.1' } 
  20.  
  21.  
  22. -- cdef 
  23. ffi.cdef[[ 
  24. void base64_encode(const uint8_t *src, size_t srclen, uint8_t *out, size_t *outlen, size_t flags); 
  25. int  base64_decode(const uint8_t *src, size_t srclen, uint8_t *out, size_t *outlen, size_t flags); 
  26. ]] 
  27.  
  28. -- define types 
  29. local uint8t    = ffi_typeof("uint8_t[?]") -- uint8_t * 
  30. local psizet    = ffi_typeof("size_t[1]")  -- size_t * 
  31.  
  32. -- package function 
  33. function _M.base64_encode(src, flags) 
  34.     local dlen   = floor((#src * 8 + 4) / 6
  35.     local out    = ffi_new(uint8t, dlen) 
  36.     local outlen = ffi_new(psizet, 1
  37.     libbase64.base64_encode(src, #src, out, outlen, flags) 
  38.     return ffi_str(out, outlen[0]) 
  39.  
  40. end  
  41.  
  42. function _M.base64_decode(src, flags) 
  43.     local dlen   = floor((#src + 1) * 6 / 8
  44.     local out    = ffi_new(uint8t, dlen) 
  45.     local outlen = ffi_new(psizet, 1
  46.     libbase64.base64_decode(src, #src, out, outlen, flags) 
  47.     return ffi_str(out, outlen[0]) 
  48. end  
  49.  
  50. return _M 

好了, 大功告成, 我们写个 demo 调用一下试试:

  1. -- main.lua 
  2. local ffi_base64 = require "ffi-base64"  
  3.  
  4. local target = "https://example.com" 
  5.  
  6. local r = ffi_base64.base64_encode(target, 0
  7. print("base64 encode result: \n"..r) 
  8.  
  9. local r = ffi_base64.base64_decode(r, 0
  10. print("base64 decode result: \n"..r) 
  1. root@router02:/data/works/libbase64-ffi# luajit -v 
  2. LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2020 Mike Pall. https://luajit.org/ 
  3. root@router02:/data/works/libbase64-ffi# luajit ./main.lua  
  4. base64 encode result:  
  5. aHR0cHM6Ly9leGFtcGxlLmNvbQ== 
  6. base64 decode result:  
  7. https://example.com 

搞定! 是不是很简单? 类似的 FFI 库还有很多, 各个语言也有不同程度的支持. 大家都可以尝试一下.

最后, 当你遇到类似的问题的时候, 就可以回忆起来, 还有 FFI 这样一件趁手的兵(魔)器(法)在你的武器库里面.

责任编辑:张燕妮 来源: 知乎
相关推荐

2009-03-25 16:54:10

Linux软件免费

2010-11-02 15:36:30

jQuery

2010-07-22 10:28:59

Web开发IDE

2020-10-29 10:43:24

网络安全支付宝电子钱包

2012-05-10 10:55:03

CSS

2018-10-25 12:01:37

机房搬迁要点

2014-01-10 14:08:57

WLAN速度优化

2017-11-01 15:50:38

数据库MySQL 8.0新特性

2016-11-11 19:51:35

2011-11-24 09:13:16

CSS

2021-05-11 10:30:34

数字化

2009-02-02 14:25:45

软件 芮祥麟 SAP

2020-09-22 07:45:14

编码语言网站博客

2012-01-09 16:43:13

点心通讯录

2019-09-24 09:29:26

Python数据接口

2014-08-25 14:40:22

百加域名

2018-05-10 08:20:23

自然语言数据集数据

2015-11-13 14:17:40

2016-10-20 16:19:51

云主机
点赞
收藏

51CTO技术栈公众号