Skip to content

RFC: ArgsManager type and range checking #22978

@ryanofsky

Description

@ryanofsky

This issue exists to explore ways of improving the ArgsManager interface to be more convenient and add compile-time type safety.

#16545 adds runtime type checking for ArgsManager options, but it isn't changing ArgsManager AddArg / GetArg interface, only adding new flags and behaviors to the existing interface, so it is not taking advantage of C++'s type safety to enforce at compile time that options are always retrieved with the same types they were registered with. A branch that implements this feature is being developed at:

And is described in (comment) and (comment).

Planned additional features are:

  • Dropping dependency on #16545 and making new interface backwards compatible with existing arguments.
  • Writing a scripted diff to port all current ArgsManager AddArg and GetArg calls to use the new interface and be backwards compatible, without changing any behavior
  • Adding helpers to read arguments directly into Options structs like BlockManager::Options MemPoolOptions without needing to list types or struct members more than once.
  • Adding support for custom types (enums, structs, and classes) so Options structs are not limited to reading bool, int, and string options.
  • Adding documentation about the implementation and how to use the interface.

This issue description has been edited since the issue was opened. The original description was:

Originally posted by @ryanofsky in #22766 (comment):

The way I would like range checking to work in the future would be to rely more on C++ types and std::numeric_limits. The idea is arguments would be registered using explicit C++ types:

const Setting<int> SETTING_myint("-myint", "description");
const Setting<std::string> SETTING_mystring("-mystring", "description");
const Setting<std::vector<std::string>> SETTING_mylist("-mylist", "description");
const Setting<std::optional<std::uint16_t>> SETTING_myopt("-myopt", "description");
const Setting<SettingsValue> SETTING_mylegacy("-mylegacy", "description");

void RegisterArgs(ArgsManager& args)
{
    args.Register({SETTING_myint, SETTING_mystring, SETTING_mylist, SETTING_myopt, SETTING_mylegacy});
}

and then they could be retrieved in a type safe way:

args.Get(SETTING_myint);    // returns int
args.Get(SETTING_mystring); // returns std::string
args.Get(SETTING_mylist);   // returns std::vector<std::string>
args.Get(SETTING_myopt);    // returns std::optional<uint16_t>
args.GetArg/GetArgs/GetIntArg/GetBoolArg(SETTING_mylegacy); // returns requested type

To get to this point, this PR cleans up existing misused flags and misnamed functions. PR #16545 adds type validation and runtime semantics without changing the ArgsManager API, and a followup PR can improve the API and update call sites without changing the semantics. (There is a direct correspondence between the ALLOW_ flags from #16545 and the useful C++ settings types bool/int/std::string/std::optional/std::variant/std::vector)

Originally posted by @ajtowns #22766 (comment):

I think I finally figured out how you can go a little bit further than the above, ending up with something like:

struct NetSettings
{
    int64_t blockreconstructionextratxn;
    int64_t maxorphantx;
    bool capturemessages;

    template<typename C, typename... Args>
    static inline void F(Args&... args) {
        return C::Do(args...,
            C::Defn( &NetSettings::blockreconstructionextratxn, "-blockreconstructionextratxn=<n>", strprintf("Extra transactions to keep in memory for compact block reconstructions (default: %u)", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS, DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN ),
            C::Defn( &NetSettings::maxorphantx, "-maxorphantx=<n>", strprintf("Keep at most <n> unconnectable transactions in memory (default: %u)", DEFAULT_MAX_ORPHAN_TRANSACTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS, DEFAULT_MAX_ORPHAN_TRANSACTIONS ),
            C::Defn( &NetSettings::capturemessages, "-capturemessages", "Capture all P2P messages to disk", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST, false )
        );
    }
};

void RegisterNetProcessingArgs(ArgsManager& args)
{
    SettingsRegister<NetSettings>::Register(args);
}

NetSettings GetNetSettings(const ArgsManager& args)
{
    return SettingsRegister<NetSettings>::Get(args);
}

class PeerManagerImpl
{
private:
    const NetSettings m_settings;
    PeerManagerImpl(..., const ArgsManager& args) : m_settings{GetNetSettings(args)), ... { ... }
    ...
};

The idea being that this way:

  • it can infer the argument type directly from the type of the struct member so that you can't accidentally specify different types between args.AddArg<int> and Get<bool>
  • that the settings are const at runtime so can be accessed without any additional locks
  • you only have to access the ArgsManager (and do string parsing) at setup time
  • you don't have to make up lots of new names for everything or add too much boilerplate

Branch built on top of this PR that has the above working at https://github.com/ajtowns/bitcoin/tree/202109-settings_struct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions