espressif / esp-idf

Espressif IoT Development Framework. Official development framework for Espressif SoCs.
Apache License 2.0
13.31k stars 7.2k forks source link

The `__attribute__((constructor))` trick not working for the Rust `esp-idf-*` crates (IDFGH-12987) #13938

Open ivmarkov opened 3 months ago

ivmarkov commented 3 months ago

Answers checklist.

General issue report

CC: @igrr @MabezDev @Vollbrecht

@igrr - sorry for the ping! Please CC whoever you deem necessary. Pinging you because the issue we are having below is quite crucial and leads to the Rust wrappers being next to unusable ATM as these are now being prone to random CONFLICT! crashes.

Overview

The TL;DR is: What I'm observing in the .map file (pasted below) of this very simple test project is that the check_rmt_legacy_driver_conflict constructor of the RMT legacy driver remains and is active for a Rust project, even if the rust project only calls the NEW RMT driver APIs (and thus we are having the dreaded CONFLICT! program abort.)

Now, what is very important to realize, is that regular, simplistic whole-.o files elimination by the linker does not really work for Rust, because - unlike in C where the definition of a codegen unit (= .o file) is exactly one .c file - in Rust it is up to the compiler, optimization settings, number of cores on the build machine and what-not as to what ends up in one codegen unit (.o file).

So it might very well be, that two completely separate Rust modules still end up in a single .o file. Like the wrapper for the legacy RMT driver, as well as the wrapper for the new RMT driver for example. You get the idea what happens next (I do realize I'm not talking about --gc-sections, but we'll cover that below, read on):

--gc-sections

Now obviously the above would've been a disaster w.r.t. Rust code size, hence by default and unconditionally Rust code is always compiled with the ffunction-sections and fdata-sections, thus all functions and so on ending up in separate sections. Rust also emits to the linker --gc-sections (Not that the link args from ESP IDF that we grab don't have it either).

So in the above scernario where we can see the code referencing the legacy and new RMT driver ending up in a single Rust .o file, I STILL see all symbols from the legacy RMT driver eliminated (check the "Discarded input sections" section in the .map file)

... except for the check_rmt_legacy_driver_conflict legacy RMT driver constructor which is still happily in there and obviously called at startup!

So my question is: is this behavior expected? I'm browsing the internet since a couple of hours, but I can't find an answer to the following question:

By the way, doing random changes to the source code layout of the sample Rust crate referenced above ^^^ (as in splitting the code referencing the legacy RMT driver in a separate file from the code referencing the new RMT driver) leads to this problem being solved. But then again, in the absence of any guarantee from Rust that codegen units do obey source file boundaries, this is a pure luck and completely unreliable.

Next steps

ivmarkov commented 3 months ago

The .map file is available here.

ivmarkov commented 3 months ago
  • Could it be, that constructors remain even if all other symbols in their .o files are eliminated by the linker --gc-sections garbage collector?

Re-reading some of the internet material from yesterday, this is the closest to our problem I was able to dig up.

It is - however - discussing how --as-needed (i.e. which dynamic libs to remain or not as refs in the final elf - we don't use dynamic libs of course) and --gc-sections interact, NOT how __attribute__((constructor)) and --gc-sections do.

Still, it seems to imply that the linker does work in two passes:

... yet, the dynamic link library will remain as referenced in the final executable. And... if it has __attribute__((constructor)) constructors, those would remain and ran when the dyn lib is loaded...

igrr commented 3 months ago

Hi @ivmarkov,

Could it be, that constructors remain even if all other symbols in their .o files are eliminated by the linker --gc-sections garbage collector?

Yes, that would be the expected behavior with GNU linker.

The linking process is well described here.

In your case, we go down the path described under "When the linker encounters a new library,..." > "If any of the symbols it exports are on the undefined list...".

Since the Rust code does reference legacy RMT driver symbols, the object file rmt_legacy.c.obj is included into the output file. Later, unused code is removed due to gc-sections, but the constructors remain.

When we added these warnings based on constructors, we did not consider it a valid situation that the project would reference both the old and the new driver.

I think we can add Kconfig option to disable the check, and you can re-implement the check on the Rust side.

(cc @suda-morris)

ivmarkov commented 3 months ago

When we added these warnings based on constructors, we did not consider it a valid situation that the project would reference both the old and the new driver.

This is an unfortunate consequence of how Rust defines "codegen units" (.o files). I was surprised myself how little control we have over that, in Rust.

I think we can add Kconfig option to disable the check, and you can re-implement the check on the Rust side.

That would work for us! I assume the CONF_ option will be back-ported to the current 5.X branches? At least to the upcoming 5.3, if not the earlier ones?