crate/module organisation: location of dummy implementations, etc.
We have various features that can be compiled out.
The "obvious" way of implementing this is to apply #[cfg]
to relevant items.
In practice, however, this results in a great proliferation of #[cfg]
:
it needs to be applied to many struct fields,
constructor parameters
(which is itsself problematic since it makes the arity depend on the config)
expressions, method calls, etc. etc.
Often a better approach is to provide standin types, when the feature is disabled. This allows much more of the code to be written the same way.
For an example, consider bridge support.
tor-guardmgr/src/bridge_disabled.rs
contains:
-
BridgeConfigBuilder
, a unit, which allows the config to be deserialised -
BridgeConfigBuilder::build
which always fails -
BridgeConfig
, a configured bridge, as an uninhabited type
As a result, the main config handling code can unconditionally
attempt to build its Vec<BridgeConfig>
from its BridgeConfigBuilder
s.
However, we don't adopt this pattern consistently, partly because there are some unsolved problems:
Dummy implementation location
When what needs to be stubbed out is a whole crate, we don't know where to put the implementation.
Options seem to include:
-
tor-basic-utils
. But that may not have access to all the needed types. And it seems an abuse of the crate. - Some other central crate. But we might run into crate hieararchy difficulties; different stubs might to mention, and define, types that live at different levels in our stack.
- Possibly several such central crates, splitting ad-hoc as needed. This seems like it would be hard to navigate.
- A dedicated stub crate for each thing we might want to stub out. But we already have dozens of crates, and this isn't just one or two more - it's a multiplying our number of crates by a constant factor >1.
- In each case in "the next crate up", and higher-layer crates should depend on that one. This will produce weird and artificial crate dependencies.
- By the to-be-stubbed crate, according to some feature flags.
This will mean moving the implementation into an internal module,
which is cfg'd and can be re-exported at the toplevel.
It might result in unwieldy source code paths,
but maybe judicious use of
#[path]
could help.
Of these I think 6 is best. In particular, it means that types referenced from super-crates are als "the same type", ie the owning crate's possibly-dummy type, ensuring additiveness.
But we need to think about what the feature structure is. We can't have a "stub-only" feature, since that's co-additive - the opposite of what we want. So I think we need an "actually does the thing" feature. Transition plans are left as an exercise to the reader.
Arc<ThingMgr>
or Arc<MutexMgr<Thing>>
Some types are Suppose ThingMgr
is stubbed out.
We want it to not impose any runtime or executable size costs.
The manager needs to be inhabited
so one of it can live in TorClient
;
we want TorClient
to have a ZST for it.
But even if ThingMgr
is a unit ZST,
Arc<ThingMgr>
incurs a heap allocation,
and the Arc
is the size of a pointer.
Worse is things like bridge_desc_mgr
in TorClient
which is Arc<Mutex<Option<Arc<BridgeDescMgr>>>>
.
Options seem to include:
- Live with the heap allocation
- Have the definer provide
ArcThingMgr
as an alias forArc<ThingMgr>
; when disabled, make it simply beThingMgr
or a new newtype? Not sure how this helps withOption<Mutex<...>>
. - keep applying
#[cfg]
I think probably 1 or 2 are good for things in TorClient
,
which is quite heavyweight anyway.
We could do 3 for fields in objects of greater multiplicity.
Error enums, ensuring additiveness
Typically we will want the stub error enum to just have a "feature disabled" variant. But to ensure additiveness we should somehow make sure that it's the same as the real enum's "not implemented" variant. Options:
-
#[cfg]
each "real" variant, ie all the ones except the "feature disabled" variant. Downside: verbose, easy to omit one and incrementally bloat the feature-disabled build. - Simply write the two enums out separately, and try to ensure similarity with tests
- Macrology of various kinds, for example a macro_rules macro that does "define error enum with the following variants and also a "feature disabled"
- (Non-option) generate just the feature-disabled variant with a macro. Macros are not supported by Rust in enum variant position :-/.
I suggest we do 1 like in tor_guardmgr::bridge::config::err
, for now.
Possibly derive-adhoc will provide a better way to do this in the future.