Build library as binary with `#[no_main]`

I might be asking something that doesn't make a lot of sense, but I'm essentially curious if it is possible to take a regular library and produce an output like if it was a binary with #[no_main].

Essentially instead of creating lib.rs + main.rs, having just lib.rs.

I can get half-way there by building with cargo rustc --crate-type bin --lib, but lack of #[no_main] in the source code prevents it from actually succeeding. Is there a way around that, like being able to inject #[no_main] from CLI directly?

Do you mean output a binary file you can use as a library, like a shared object or a static lib? If so, you're probably looking for one of the crate types staticlib, dylib, or cdylib. bin is for executable files, which makes no sense without a main since there's nothing to execute.

This is for custom "bare metal" target, so it has to be a binary, not a library, and there is no main there

In that case, I don't believe there's any good way to accomplish what you want. You could maybe put no_main behind a cfg gate to only enable it when you're building a binary, but you're still jumping through hoops. Is there a particular reason you need this? It's usually considered bad form to put executable related logic into a library interface.

It'll be running in a VM, just has to be an ELF binary that is further processed by VM-related tooling.

I can just add #[no_main] to a library and it'll work just fine, but since I'm working on tooling that other developers would use, having it in a library will raise eyebrows, so I was wondering if I can make it look like a library for developers, but still behave the way I need it to.

If it's a binary, and meant to be used as such, then why try to make it look like a library? If it's both, why try to lump the two together in the code? In any case no, if your goal is to avoid no_main, you cannot. If you're building a binary without a main, then you must have it. And in my opinion, lying to your users about what they're looking at isn't worth it anyways.

I don't know why you want to do this, but you can build a binary crate as library:

# Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

# `lib.name` is optional, default to `package.name`
[lib]
path = "src/main.rs"

# because the path is not in the form
# `src/bin/foo.rs` or `src/bin/bar/main.rs`
# `bin.name` attribute is required
[[bin]]
name = "app"
path = "src/main.rs"
$ cargo build --lib
$ cargo build --bin app

as for #![no_main], this is necessary when building binaries for bare metal targets, and it has no effect when you build the crate as a library.

I have had the most success using a main.rs and compiling a binary. I use a macro such that:

// copies in `_start`, allocator, etc.
vm::init!(main);

fn main() {
    // consumer's code here
}

Even on a bare metal target you need an entry point. Usually you can use the attribute #[start] to mark a function as such. Using crate type staticlib works in many cases, but then you don't need #[no_main]. If you need finer control, you may write your own linker script.
In almost all cases, pure cargo is not sufficient, but you need a special build script.

Bare metal was in quotes. VM has additional information that allows it to call into necessary functions directly, it is not running on a physical. But all that is irrelevant to the question here, I know what I'm looking for and library or custom entry point is not it. I'm just looking for a way to not write #[no_main], it is fine if there isn't any.

To be clear, is the issue with the normal Rust set-up for this (where you have a lib.rs and a main.rs that uses it) that it just won't work here because the main.rs would be empty and wouldn't pull in any of the required symbols?


I might be off-base here, but isn't the only real difference between ELF binaries and shared objects that the former has e_type set to executable, the start address is set, and otherwise it's just conventions about what sections are present? That might be a cleaner path, depending on how much control you have over that preprocessing.

If I have main.rs with #[no_main] everything works fine. I hope this answers some of the questions: polkavm/guest-programs/build-examples.sh at 899690f25c4d4ac4dcaaca44a1aed7799376b992 · paritytech/polkavm · GitHub

Do you mean like this example, with only a main.rs? polkavm/guest-programs/example-hello-world/src/main.rs at master · paritytech/polkavm · GitHub

I was talking about the setup that you normally have for a single crate that has both a library interface and a binary is that it has all the library code in lib.rs and also a main.rs that pulls in itself as a library. But here that probably won't work, as main.rs wouldn't actually reference anything, so the library code wouldn't get linked in.

Perhaps there's some setup that gets it to do so properly, though?

Yes, that is the correct example.

It is possible to have both lib.rs and main.rs and it will work, just more clutter. For the use case I have for that VM it will look a feel like a library as far as developer is concerned: you either run it i a VM, in which cases you have multiple entrypoints (one for each annotated function) or you include it as a dependency in other libraries. The only time when main.rs and #[no_main] is needed is during intermediate compilation step.

So if I could hide #[no_main] from a user too (so looks as close to a regular library as possible) then I would.

#[start] will be removed in 1.86.

no_std platform-specific programs should use #![no_main] and define their own platform-specific entrypoint symbol with #[no_mangle], like main, _start, WinMain or my_embedded_platform_wants_to_start_here.

4 Likes

Makes sense, so what about using a cdylib (ELF SO) vs bin angle? If what you want is essentially to pull out exports instead of running a start function that's pretty much what they are, but I'm not sure what sort of processing you're actually doing here.

The processing is being done by the tooling of that project. I tried (and succeeded) in feeding it an object file using --emit=obj, though the name is unpredictable and I'm not exactly sure it contains everything the tooling expects in non-trivial case. For .so I'm getting the following error:

error: cannot produce cdylib for example-hello-world v0.1.0 (polkavm/guest-programs/example-hello-world) as the target riscv32emac-unknown-none-polkavm does not support these crate types

Makes some sense, I suppose, as it doesn't have a loader that could resolve any imports, but that's also technically true for binaries.

I have not found a way to build the binary, but I did find a way to what I need to do with an extra step of linking the static library:

ld -relocatable \
    --whole-archive libexample_hello_world.a \
    -o libexample_hello_world.o

This produced an object file that turned out to contain everything that the tooling I was feeding it into afterwards expected, all without #[no_main].

The only difficulty is that since my target is riscv64, I had to install cross-compilation tooling and use riscv64-linux-gnu-ld, I wish there was a way to ask rustc to produce the same object file or link a few files for me on request instead.

The drawback here is that both .a and .o files include a bunch of useless compiler built-ins, which contributes ~4M for 32-bit build and ~6M to 64-bit build. Executable doesn't have that and is measured in tens of kilobytes in size instead.