symbol _embassy_time_now is already defined
主线位置:定时器主线的「
Timer把到期时刻和 waker 交给时间驱动」。全程序只能有一个驱动,因为驱动是一个链接期符号(时间驱动);本页讲出现两个时会怎样。一个都没有的情况见 Undefined symbols: _embassy_time_now。
flowchart TB
two["两个 time_driver_impl!"] --> where{"在哪"}
where -->|同一个 crate| e1["rustc 报错"]
where -->|两个 crate| ld{"链接器"}
ld -->|rust-lld| e2["duplicate symbol"]
ld -->|macOS ld| w["警告或无声"]
w --> pick["用先出现的那个"]
实验 crate exp-embassy/pita-dup、pita-dupdev、pita-dupdev-fixed(std)和 pita-tim(thumbv7em-none-eabihf);M2 Max、macOS 27、rustc 1.99.0。
现象
同一个 crate 里的两个驱动。 embassy-time 同时开 std 和 mock-driver(pita-dup --features mock,debug),编译 embassy-time 自己就失败:
error: symbol `_embassy_time_now` is already defined
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-time-0.5.1/src/driver_std.rs:20:1
|
20 | / embassy_time_driver::time_driver_impl!(static DRIVER: TimeDriver = TimeDriver {
21 | | inner: Mutex::new(Inner{
22 | | zero_instant: None,
23 | | queue: Queue::new(),
24 | | }),
25 | | signaler: Signaler::new(),
26 | | });
| |__^
|
= note: this error originates in the macro `embassy_time_driver::time_driver_impl` (in Nightly builds, run with -Z macro-backtrace for more info)
error: could not compile `embassy-time` (lib) due to 1 previous error
没人会故意这样写。真实的来路是测试:正常依赖开 std,[dev-dependencies] 里再写一次 embassy-time 开 mock-driver,想在测试里手动推进时间(pita-dupdev):
[dependencies]
embassy-time = { version = "=0.5.1", features = ["std"] }
[dev-dependencies]
embassy-time = { version = "=0.5.1", features = ["mock-driver"] }
cargo build 和 cargo run 都正常(打印 now = 4 ticks),cargo test 报上面那条错误。dev-dependencies 的 feature 只在构建测试、例子这类目标时才和正常依赖合并,所以问题只在跑测试时出现。
两个 crate 里的两个驱动。 比如 HAL 开了 time-driver-any,二进制里又留着自己写的驱动。pita-tim(embassy-stm32 开 time-driver-any)的 bin dup_systick 是模板的 SysTick 驱动程序,cargo build --release:
= note: rust-lld: error: duplicate symbol: _embassy_time_now
>>> defined at lib.rs:168 (~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-time-driver-0.2.2/src/lib.rs:168)
>>> ~/exp-embassy/target-pita-qemu/thumbv7em-none-eabihf/release/deps/dup_systick-b2c4f2faa4a5635a.dup_systick.eec1443e62c15ad0-cgu.0.rcgu.o:(_embassy_time_now)
>>> defined at lib.rs:168 (~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-time-driver-0.2.2/src/lib.rs:168)
>>> embassy_stm32-0c67ec75b754013f.embassy_stm32.fd2545e22b51d66f-cgu.2.rcgu.o:(.text._embassy_time_now+0x1) in archive ~/exp-embassy/target-pita-qemu/thumbv7em-none-eabihf/release/deps/libembassy_stm32-0c67ec75b754013f.rlib
rust-lld: error: duplicate symbol: _embassy_time_schedule_wake
...
两处都写 lib.rs:168,因为两个定义都是 time_driver_impl! 展开出来的,位置记在宏里;真正有用的是后面的目标文件名:一个是 dup_systick,一个在 libembassy_stm32 里。
macOS 上同样的结构不一定报错。pita-dup --features own:embassy-time 开 std,二进制里再用 time_driver_impl! 定义一个 now() 恒为 42 的驱动,main 睡 20 ms 后打印 Instant::now():
warning: linker stderr: ld: duplicate symbol '__embassy_time_now' in:
~/exp-embassy/target-pita-std/debug/deps/pita_dup-490883b3c6b7061d.9dw2rdz4jfctd8enhozrmlq64.0u4bfik.rcgu.o
~/exp-embassy/target-pita-std/debug/deps/libembassy_time-37093cb091fae193.rlib[4](embassy_time-37093cb091fae193.embassy_time.6329b00ed89fe955-cgu.0.rcgu.o)
|
= note: `#[warn(linker_messages)]` on by default
warning: `pita-dup` (bin "pita-dup") generated 1 warning
now = 42 ticks
release 构建连这条警告也没有,同样打印 now = 42 ticks。程序能跑,用的是二进制里那个驱动,std 驱动被丢掉了。为什么 debug 有警告、release 没有,见时间驱动末尾对 rlib 归档的分析。
出处
两个错误都出在 time_driver_impl! 生成的 #[unsafe(no_mangle)] fn _embassy_time_now():同一个 crate 里两个同名 no_mangle 函数,rustc 直接拒绝;分在两个 crate 里,rustc 各编各的,到链接时才相遇。embassy-time 的 Cargo.toml 里 mock-driver、std、wasm 三个 feature 各打开一个驱动文件,彼此之间没有互斥检查;模块文档说「If there is none or multiple drivers in the crate tree, linking will fail.」,macOS 上的结果说明后半句不总成立。
修法
测试要 mock 时钟,就别让 std 驱动无条件打开。pita-dupdev-fixed 把它放进一个默认 feature:
[dependencies]
embassy-time = "=0.5.1"
[features]
default = ["real-time"]
# the std driver only when the real clock is wanted
real-time = ["embassy-time/std"]
[dev-dependencies]
embassy-time = { version = "=0.5.1", features = ["mock-driver"] }
#[test]
fn mock_clock_moves_only_when_told() {
let t0 = Instant::now();
MockDriver::get().advance(Duration::from_millis(5));
assert_eq!(Instant::now() - t0, Duration::from_millis(5));
}
cargo run 照常用 std 驱动(now = 0 ticks,零点在第一次调 now() 时定下);cargo test 仍然报 already defined,因为默认 feature 还开着;cargo test --no-default-features:
test tests::mock_clock_moves_only_when_told ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
另一种做法是把用 mock 的测试放进一个单独的 crate,它只依赖 mock-driver。mock 驱动的时钟是全局的,同一个测试二进制里并行的测试会互相推时间,见时间驱动的「mock 驱动」。
芯片上:HAL 的 time-driver-* 和自己的驱动二选一。pita-qemu 去掉 time-driver-any、只留 SysTick 驱动,同一份程序链接通过,在 QEMU 里正常打印 tick。
怎么确认没中招
cargo tree -e features,normal,dev -i embassy-time-driver(带上dev才看得到测试时的图,pita-dupdev里"mock-driver"和"std"两条分支都在)里,提供驱动的分支只能有一条:embassy-time feature "std"/"mock-driver"/"wasm"、embassy-stm32 feature "_time-driver",或者自己代码里的time_driver_impl!。- 在 macOS 上开发、给芯片交叉编译时,不要把「本机能链接」当成证据;把
linker_messages警告当错误看。 - 跑起来以后看时间是否在走:
Instant::now()恒定或单位不对(std 驱动 1 µs 一个 tick),多半是用错了驱动。
小结
- 两个驱动在同一个 crate 里,rustc 报
symbol _embassy_time_now is already defined;最常见的来路是 dev-dependencies 给cargo test加了mock-driver。 - 分在两个 crate 里,rust-lld 报
duplicate symbol;macOS 的 ld 在 debug 下只警告、release 下不出声,程序用其中一个驱动继续跑。 std驱动放进可关的 feature,测试用--no-default-features;芯片上 HAL 驱动和自写驱动只留一个。