How to hide Tokio and private-generated code from the developer's code?

I want to wrap the entry point Rust so that the program passes by some initial processes, including being wrapped by tokio::main for its async. event loop.

I want to do 2 things:

  • Not require the user to put tokio as a dependency in their Cargo.toml. Instead, they will only require my platform's crate as a dependency.
  • Have a clean, empty Rust entry point:
fn main() {
    // entry point
}

I've at least one requirement in these 2 processes:

  • Generate private Rust code dynamically (which is include!d in the runtime code) to setup some internal data, such as the developer's application identifier, used for determining the application's installation path in different platforms (Android, iOS, OSX, Windows, Linux and gaming consoles) for use in my framework APIs, including the file system's app: URI.

I tried doing so earlier by creating a macro in the form:

myplatform::main(async move |app| {
    // entry point
});

That expands to a main item.

However, when you get a rustc diagnostic inside that lambda, it gets dicey. If I recall correctly, the diagnostic gets attached to the entire lambda instead of the actual line where the diagnostic belongs.

Here's the previous experiment: https://github.com/rialight/api/tree/master/proof/src/sample_app

I know of the attribute macro, but it requires a separate crate, difficulting the requirement above to generate Rust code dynamically (which goes into the build recipes directory):

#[myframework::main]
fn main() {
}

Any ideas of what else to do?

I believe your goals are at odds with the language's goals.

1 Like

Ahem, what can I do to know the app's directory in Android then? /data/anything? Where will the id come from, considering it comes from the project settings ([package.metadata.myframework]'s id in Cargo.toml)?

Stop trying to hide things. The language isn't suited to that mindset.

But if you come to think of it, tokio::main already hides boilerplate around your entry point. And what I'm going to hide doesn't affect the whole crates.io ecosystem that depends in my crate: it will only affect entry point projects (applications).

At the other side, if I try not hiding the boilerplate, besides still hiding some (due to wrapping entry in tokio::main), it'd not be comfortable for a developer to create their application in my graphical platform using something along the lines of:

main.rs

#[tokio::main]
fn main() {
    include!(concat!(env!("OUT_DIR"), "/myframework_generated_code.rs"));
    // entry point
}

I could also warn that main.rs must not be touched and instead refer to another file that is referenced by it. Like I said this is not comfortable.

Another point: my framework will have a CLI for creating new projects from empty templates, but I wanted a clean project structure.

I could use an attribute macro like myframework::main, but since it has to be in another crate, it'll end up not including the generated code with that include! macro... or will it?

My guess: it might end up caching that generated code and if that generated code is updated (due to a change in the project's id), it won't be reflected.

#[tokio::main] can be applied to any function, and will cause calls to that function to be wrapped in a helper which creates a Tokio runtime. So, this might work for you:

#[tokio::main]
async fn main() {
    // entry point
}

Callers can treat main like an ordinary synchronous function; the function body (which is yours) can treat main like an async function and perform awaits freely.

Only if by "hide" you mean "remove from the editor." It doesn't make that code hard to discover - macro expansion will reveal exactly what that attribute macro is generating for you. I don't really think of it as hiding, and I don't think the tokio team does, either - it's abstraction, in that it replaces a complex collection of ideas with a single simpler one, but it's not concealment. Tokio works just fine if you read what the #[tokio::main] attribute generates and write your own version.

3 Likes

Hmm, I'd like to do the same as tokio::main (the same attribute macro) though. Just afraid include! will not work properly as it may resolve in the wrong context?

If your attribute macro emits an include, then that include will be expanded in the context of the generated program, and not in the context of your attribute macro's source code, that's true. There are a few ways to ensure that the generated code works as expected regardless of where the attribute is expanded; the most straightforwards is to generate code that uses full item paths and which includes all of the code necessary without further macro expansion.

If there are parts of the implementation of your attribute macro that you'd like to simplify, putting them in functions, structs, &c in your own crate and referencing them that way is more reasonable than trying to include! that code, in most cases. If you can write out at least a sketch of how you'd expand your attribute macro, we can probably offer suggestions on how to do that.

1 Like

That means an attribute macro may work for me then, as I need to read the project's configuration file at build time.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.