Writing a library with "hook" functions for the app to fill in

I'm writing a sort of "framework" library which will provide the program's real entry point, do some setup work, and then call into the application.

(If you're curious, it's analogous to crt0 in an embedded application.)

I have attempted to describe the situation thusly:

// In the framework code
fn actual_main() -> ! {
extern {
  fn app_main() -> !;

// In the app
fn app_main() -> ! {
  loop {}

This compiles, but it poses an interesting problem to the linker, because the circular symbol dependency is not properly expressed. The C-style linker is sensitive to the order of objects/libraries on the command line; in general, undefined symbol references must appear before the symbols that resolve them. And Cargo generates a properly ordered linker command line based on the information it has, but it doesn't know what I'm doing, and I invariably get

src/arm_m/exc.rs:115: undefined reference to `app_main'

because it sees main first, and then the library.

(If you're curious I have a longer writeup explaining this situation here.)

There are several ways to express this to the linker.

  • "Groups," where several objects are explicitly marked as interdependent, causing the linker to iterate the symbol resolution process. This is a big hammer that hurts link time.
  • Including an explicit undefined symbol with -u app_main. This is what complex embedded applications built using Cobble usually do.
  • And probably others.

I can get rustc/ld to do what I want, but not through Cargo. I'm curious if there's either a way I can configure Cargo to make this work (perhaps by injecting linker flags into the build of any component depending on my library?), or if you can propose a better factoring of the code to make the problem unnecessary.

To be clear, I do not want any application code to run before the library entry point, and while I'd ideally like the call to be resolved statically, I'm feeling flexible on that.

How are you doing this? Doesn't cargo require the entry point to be in the main crate?

I'm assuming you don't like this for some reason:

fn actual_main<F: FnOnce() -> !>(app_main: F) -> ! {

With a big stack of #[no_main] and whatnot you can get it to tolerate a missing conventional entry point; the true entry point of my application is the processor's vector table. (As a simpler example, if you're curious, this code has no Cargo-visible entry point.)

I like it, but I'd have to call it from an application function, which means that function is running before this code. The library startup routine in question is setting up initialized data and doing things like turning on RAM, so it's pretty hard to reason about whether it's safe to do that from uncontrolled app code. Example:

fn main() {
  println!("Welp, now I've hosed myself.");

Now I think of it, I should be in the same situation with sgx-utils. The "app" depends on libenclave, but libenclave depends on entry in the "app". I don't know why I don't run into your linker issue. I think it might be because (a) the slew of linker options I pass or (b) I'm building a shared library, not an executable. Although I recently did experiment succesfully with building an executable instead and I don't remember running into this particular issue.

Edit: or (c): the extra "global extern" magic related to #[no_mangle] fixes he issue.

You know, this makes me wonder if there's a general use for "framework" crates. After all, main isn't really the entry point; that's in std. Then you have unit tests where, again, the actual entry point isn't in your code. I know there are GUI libraries that really, really don't want you doing anything before you let them run their code, due to thread affinity.

Perhaps there's a useful general abstraction here waiting to be uncovered. Perhaps a "framework" crate type that can see symbols in its reverse dependenc{y,ies}.

1 Like

I've explored options like this in my build systems at work, for cases like "app depends on RTOS, but RTOS needs hook functions implemented in app." One approach is to allow dependencies to be parameterized, e.g. (in pidgin Rust) "this crate needs a crate defining entry but the user can choose what it is."

Getting correct reproduceable hermetic parallel incremental builds in thorny situations like this was what motivated Cobble, but I'm really trying to take Cargo as far as I can for now.

Edit: it occurs to me that this situation is similar to pluggable panic/allocator crates.