serialize()方法_解决Alpine上Rust无法使用过程宏的方法

背景

过程宏

Rust的过程宏(Procedural macro)是一个挺好的设计,我们实际工程中的许多地方都会用到它。比如说:

  • 使用serde序列化时
#[derive(Serialize)]
struct Foo {
    bar: usize
}
  • 使用actix进行路径匹配时
#[get("/")]
pub fn index() -> String {
    "Hello".to_owned()
}

此外还有很多地方也会用到。由这些例子可以知道,如果我们将Rust作为后端的程序,那么过程宏几乎是必不可少的。

Alpine

相信许多人都是在使用Docker之后才知道这个Linux操作系统的。由于Docker倡导「一个容器执行一个服务」的理念,所以镜像的操作系统应该尽可能地适应相应的服务,而非包罗万象。而Alpine以"Small,Simple,Secure"的3S口号为理念,其镜像仅仅只有5.55MB. 因此,Alpine作为镜像操作系统十分受欢迎。

问题

Rust的过程宏和Alpine都是如此之好,那么,把它们结合一下会怎样呢?

我这里使用的Rust版本为rustc 1.43.0, Alpine内核版本为5.3.0-51-generic.

我们首先写个demo:

# Cargo.toml
[package]
name = "demo"
version = "0.1.0"
edition = "2018"

[dependencies]
serde= { version = "1.0", features = ["derive"] }

其主函数只需声明即可:

// src/main.rs
fn main() {}

然后,我们为了把它放在Alpine里,写一个Dockerfile:

FROM rust:alpine AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM alpine
WORKDIR /app
COPY --from=builder /app/target/release/demo ./demo
CMD ["./demo"]

毫无疑问,这是最简单的一个Dockerfile的写法了。

问题一

那么,我们此时运行,会出现什么问题呢?

$ cargo build
error: cannot produce proc-macro for `serde_derive v1.0.106` as the target `x86_64-unknown-linux-musl` does not support these crate types

哦豁,出大问题。

解决一

首先,我们得搞清楚出这问题的原因。

第一,是Rust的过程宏的原理。众所周知,宏就是在编译器编译阶段运行的用户代码,用Rust文档中的话来说,就是一个AST到AST的函数。在Rust的实现中,过程宏的crate的类型必须在Cargo.toml中声明为proc-macro:

[lib]
proc-macro = true

而这类的crate,在编译器编译时,是会由rustc动态链接的。

第二,是Alpine的最显著特征。Alpine最显著的特征就是其使用了musl作为标准库libc的实现,而我们熟悉的Ubuntu等符合GNU的操作系统则使用的是GNU-libc. musl-libc的口号就是可嵌入的标准库,其大小特别小。因此,在被Rust链接时,会默认进行静态链接。

第三,Rust在除Windows平台之外不支持将动态链接库与musl-libc静态链接。

容易看出来,第一点、第二点同第三点是矛盾的。如果将musl-libc静态链接,那么就无法编译过程宏,因为其是动态链接库。

解决方法有两种:

  • 让我们的程序不静态链接到musl-libc
  • 让过程宏的crate不静态链接到musl-libc

第一种方法是只需要我们修改编译的指令即可,第二种方法却要修改编译器或者过程宏的代码。因此,作为普通用户,我们使用第一种方法(Rust 1.44使用了第二种方法解决了这个事情)。

解决方法就是关闭rustc在链接时crt-static的flag, 让我们可以动态链接到musl-libc:

RUSTFLAGS="-C target-feature=-crt-static" cargo build

这样,就可以顺利地进行动态链接从而编译了。

问题二

在设立了-crt-static的flag之后,我们再一次尝试了编译。这时候,居然又出现了新的错误:

error: linking with `cc` failed: exit code: 1
....
....
....
  = note: /usr/lib/gcc/x86_64-alpine-linux-musl/9.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find Scrt1.o: No such file or directory
          /usr/lib/gcc/x86_64-alpine-linux-musl/9.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: cannot find crti.o: No such file or directory
          collect2: error: ld returned 1 exit status
....
....
error: could not compile `syn`

解决二

根据错误报告,我们可以发现,这次错误的原因是少了crti.o这类的,一般静态链接到程序里的bootstrap库。根据经验,这些库是位于操作系统C库的开发包里,对于Alpine来说,就是musl-dev这个包。因此,我们只需要在编译的环境中安装开发包

apk add --no-cache -U musl-dev

即可。由于这些程序是静态链接的,因此我们在运行环境中不需要安装这样的包。

问题三

看似很顺利地解决了这个问题,我们的demo程序也顺利地编译出来了。而通用的Docker使用方法都是多阶段构建,也就是一个镜像只用于构建程序,另一个镜像才是真正的跑程序的,这样可以最大程度减少镜像的体积。那么,我们修改了cargo编译的条件,还是按照最开始的Dockerfile进行构建,结果会是怎样呢?

第一阶段,很顺利地结束了。第二阶段,却又出错了:

$ ./demo
Error loading shared library libgcc_s.so.1: No such file or directory (needed by ./demo)
Error relocating ./demo: _Unwind_Resume: symbol not found
Error relocating ./demo: _Unwind_SetIP: symbol not found
Error relocating ./demo: _Unwind_GetRegionStart: symbol not found
Error relocating ./demo: _Unwind_GetTextRelbase: symbol not found
Error relocating ./demo: _Unwind_RaiseException: symbol not found
Error relocating ./demo: _Unwind_GetIP: symbol not found
Error relocating ./demo: _Unwind_GetIPInfo: symbol not found
Error relocating ./demo: _Unwind_GetDataRelBase: symbol not found
Error relocating ./demo: _Unwind_SetGR: symbol not found
Error relocating ./demo: _Unwind_Backtrace: symbol not found
Error relocating ./demo: _Unwind_GetLanguageSpecificData: symbol not found

看样子是重定位的错误,所以,居然,居然又是链接的错误???

解决三

我们仔细看一下错误信息,知道了错误是出在libgcc_s.so.1上。libgcc是干什么的呢?从它的官网上我们可以看到一句话:

The dynamic linker must be able to find libgcc.

噢!这一下,全部都水落石出了。我们之前因为把musl-libc改成了动态链接,那么在这里,就自然需要动态链接器来链接了。而按照官网的说法,动态链接器一定需要libgcc才能运行。但是,因为这是一个全新的Alpine镜像,它居然不自带libgcc, 所以就出错了。而正常来说如果是静态链接musl,那么也就不需要动态链接器,那么缺少libgcc也不会报错。

因此,我们只需要在执行前下载libgcc即可:

apk add --no-cache -U libgcc

Rust 1.44

Rust 1.44解决了musl环境下过程宏静态链接到libc的问题。那么,由于写本文的时候stable版本还没出来,我专门下载了rustlang/rust:nightly-alpine这个镜像来进行实验。

实验结果是,确实不需要设立-crt-static的flag了,但是,问题二和问题三依然存在。

最终解决方案

根据上面的讨论,我们最终的Dockerfile如下:

对于Rust 1.43及以下版本为:

FROM rust:alpine AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache -U musl-dev
RUN RUSTFLAGS="-C target-feature=-crt-static" cargo build --release

FROM alpine
WORKDIR /app
COPY --from=builder /app/target/release/demo ./demo
RUN apk add --no-cache -U libgcc
CMD ["./demo"]

对于Rust 1.44及以上版本为:

FROM rust:alpine AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache -U musl-dev
RUN cargo build --release

FROM alpine
WORKDIR /app
COPY --from=builder /app/target/release/demo ./demo
RUN apk add --no-cache -U libgcc
CMD ["./demo"]

参考

  • rust-lang/cargo#7563
  • rust-lang/cargo#7564
  • rust-lang/rust#34987
  • rust-lang/rust#40113
  • rust-lang/rust#40174
  • withoutboats/fehler#41
  • libgcc背景
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值