Best practices for mutually exclusive crate #[cfg(feature = ...)] APIs?

I'm writing an application divided roughly as follows:

client/ -- The client application
server/ -- The server application
shared/ -- Common libraries used by both

The root of this has a Cargo.toml workspace that encompasses all of the crates in question.

Out of necessity, some types and functions in shared are controlled by the features "enable_client" and "enable_server". In particular, some of that functionality is mutually exclusive between the two of them, e.g.

#[cfg(feature = "enable_client")]
pub type ClientOnlyType = SomeClientOnlyStruct;
#[cfg(not(feature = "enable_client"))]
pub type ClientOnlyType = ();

This is unfortunately unavoidable with how I'm using generics and proc macros for code generation (believe me, I've tried, the alternatives are dreadful). The issue I'm running into is that cargo check appears to unify all features in workspaces (rust-lang/cargo#11704) and so I get false-positive errors in either my client or server depending on which types are enabled for each. Each app compiles fine, but my VSCode is riddled with red highlights and it makes it difficult to diagnose actual problems.

Is there any way I could structure my workspace better here to avoid this workspace feature unification problem, or any better solution overall without changing the code structure itself? The only tool Rust provides for command-line-based conditional compilation is features, and I know they're only intended to be additive and semver-safe, but Rust provides no alternative here for a non-additive use case.

Workspaces are designed to share features. For this situation, your only alternative is to not use a workspace.

This does not stop you from using path dependencies, and it also does not stop you from using rust-analyzer (if I understand correctly, it will search for packages/workspaces one directory deep automatically, and if that doesn't work, you can set rust-analyzer.linkedProjects).

1 Like

it also does not stop you from using rust-analyzer (if I understand correctly, it will search for packages/workspaces one directory deep automatically, and if that doesn't work, you can set rust-analyzer.linkedProjects ).

Is there a way to do so in a single VSCode workspace? Having to switch between a client VSCode workspace window and a server VSCode workspace window is what I'm trying to avoid.

The instructions I wrote are for exactly that. Open the parent directory (without Cargo workspace) and it should work; and if it doesn't, configure rust-analyzer.linkedProjects in a .vscode/settings.json.

(This may unfortunately result in duplicated diagnostics; I filed an issue for that.)

1 Like

So, I've converted my project into three parts: the client application crate, the server application crate, and a workspace containing all shared crates. It works okay. The false-positive errors are gone, but I've run into two issues.

  1. I need to individually run commands like cargo fmt on each of the three individually, which gets pretty tedious. I guess I could fix this with scripts.

  2. In VSCode, rust-analyzer will gray out code that is exclusive to either client or server. It seems to do so according to which downstream crate is bottom in the list of linked projects (so if server is last, it grays out all client code in the common crates). Nothing I've tried so far has let me get around this. So far I've tried:

    • Setting both client and server as default features in all upstream common crates
    • Adding both client and server to rust-analyzer.check.features and rust-analyzer.cargo.features

Any idea what I can do here to fix either of these issues? Maybe I could ditch the workspace for shared code, but it would make issue 1 even more tedious.

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.