-
Notifications
You must be signed in to change notification settings - Fork 37.7k
rpc: Add MaybeArg() and Arg() default helper #28230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
Concept ACK |
fa13ff4
to
fa8cb8b
Compare
fac0196
to
fa6b2da
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approach ACK fa6b2da
This makes for a much more elegant interface throughout the RPC methods, with minimal overhead.
fa6b2da
to
fa5468d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK fa5468d, really like this!
Concept ACK |
faa8309
to
fa56ad7
Compare
src/rpc/util.h
Outdated
* Helper to get a request argument or its default value. | ||
* This function only works during m_fun(), i.e. it should only be used in RPC method implementations. | ||
* Use Arg<Type> to get the argument or its default value. | ||
* Use Arg<Type*> to get the argument or a falsy value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "rpc: Add Arg() default helper" (fa56ad7)
Haven't really looked at implementation yet, but can documentation be updated to say what happens with type mismatches? E.g. if arg is an int and you request a bool? Is the value just ignored and is a default returned instead? Is there an exception?
Also might be nice to add a short examples here showing what equivalent low level code is so developers can know how to use this helper and interpret it. Like Arg<int>(3)
is equivalent to request.params[3].isNumber() ? request.params[3].getInt<int>() : default_value
(or whatever the equivalent is)
Overall this seems like a nice improvement that should cut down on boilerplate. I am wondering if it might be possible later to extend this:
- to support arrays/objects
- to enforces only expected types are passed at compile time
- to enforce default not requested when there is no default at compile time
- to somehow be compatible with luke's RPC/Wallet: Convert walletprocesspsbt to use options parameter #24963 which allows multiple types per parameter (if that is a good idea)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to enforces only expected types are passed at compile time
Do we want to revisit #22978 first before doing compile time type checking for RPC?
I think compile time RPC type checks would require templatising RPCArg
by what is currently m_type
(and m_inner
perhaps), changing RPCHelpMan
to be templatised based on a std::tuple<...>
of RPCArg<X>
s, and likewise for RPCMethodImpl
which then gives the actual RPC implementation compile time access to its expected argument types.
I don't think you could reasonably access params by name and get compile time type checking; for the named-only params via options
, you could turn options
into an ordered tuple and access named-only params by their position from when you defined the options
object.
For multitype parameters, I think it'd be best to just have a Type::ANY
placeholder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't really looked at implementation yet, but can documentation be updated to say what happens with type mismatches?
This sound like a bug in the source code, so I don't think it makes sense to document unreachable or unexpected code paths. Generally, in the RPC server non-fatal bugs are expected to throw an internal bug message.
I haven't looked at the code, but types of arguments passed in by the user should be checked before m_fun is run. m_fun, and thus, Arg()
isn't run when the types passed in by the user mismatch the ones in the documentation. If Arg()
specifies the wrong type, the getter will throw. Currently all getters are univalue getters, so they'll throw the univalue internal exception. (This is the current behavior and preserved)
I think this is fine as-is, but I am happy to change it to something else, for example:
- Re-implement the type check and throw a new, special exception. (This will be a change in "behavior", or rather "bug behavior")
- Implement a compile-time json and use the compiler to avoid the case altogether, or alternatively, use clang-tidy to do it?
Also might be nice to add a short examples here showing what equivalent low level code is so developers can know how to use this helper and interpret it.
The diff in this commit should explain it. I've also added docs about the internals (isNull
check and fallback to RPCArg::m_fallback)
to support arrays/objects
Yeah, it should be possible to return an object whose operator[]
is defined. Would it be fine to do this in a follow-up?
to enforces only expected types are passed at compile time
Maybe as follow-up, unless the patch is trivial?
to enforce default not requested when there is no default at compile time
This is already done, no?
to somehow be compatible with luke's
Not sure about adding dead code, but once and if it is needed, it should be trivial to implement by mapping the types to a Arg<std::variant<int, bool>>(1)
in C++ code. However, I am not sure if this is worth it, when it is only used once. Might as well use the "legacy" method for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think compile time RPC type checks would require templatising
RPCArg
by what is currentlym_type
(andm_inner
perhaps), changingRPCHelpMan
to be templatised based on astd::tuple<...>
ofRPCArg<X>
s, and likewise forRPCMethodImpl
which then gives the actual RPC implementation compile time access to its expected argument types.
I haven't looked at this, but some RPCHelpMan
have run-time inputs, so I am not sure if they can be trivially converted to compile-time inputs. Maybe this can be done in a follow-up? The diff should be a trivial change from Arg<int>(0)
to Arg<int, 0>()
, right? Or with future C++ versions no change in m_fun
implementations is needed at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sound like a bug in the source code, so I don't think it makes sense to document unreachable or unexpected code paths.
If it's a bug to to call the the Arg function with an unexpected type, it's important for the documentation to at say what types it is allowed to be called with, because I don't think there's another way of knowing this without a having a pretty deep understanding of the RPCHelpMan framework and digging into template and macro code.
If you just want the documentation to say "It is a bug to pass X type" instead of "Passing X type will throw an exception" that is fine of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's important for the documentation to at say what types it is allowed to be called with
I guess long term it could make sense to make RPCArg::Type
an enum of C++ types and then have the documentation say that the type passed to Arg<Type>(i)
must match exactly the corresponding RPCArg::Type
.
For now, my recommendation would be to call Arg<std::string>(i)
for STR*
args, Arg<bool>(i)
for BOOL
args, and Arg<double/(u)int${n}_t>(i)
for NUM
args.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be fine to add "The Type
passed to this helper must match the corresponding RPCArg::Type
" to the doxygen of Arg
?
Edit: Done.
src/rpc/util.cpp
Outdated
return &std::get<RPCArg::Default>(fallback); | ||
} | ||
|
||
#define TMPL_INST(no_default, ret_type, get_arg, return_code) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could remove no_default
from the function signature here, and deduce it from the ret_type
being a std::optional or pointer type? I think both approaches have merit, so no very strong preference either way atm, but since we're already doing those deductions in Arg
I think it would make sense here, too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, mind providing a patch that compiles, which I can take?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for extending this to cover all kinds of argument types, I think this will make it much more straightforward for users to add in their own template instantiations where needed.
I still don't really have a clear preference for std::optional vs pointer, and I think it's not that important, so ACK fa56ad7
fa448d5
to
faac216
Compare
Addressed all (style) feedback so far, I think 😅 |
utACK faac216 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK faac216
nit: pull description & title have gotten a bit outdated as the PR changed quite a bit since its initial version. I think a brief mention of MaybeArg
and the optionality of arguments could be helpful to include here?
As a nice bonus, it seems adding the RPCHelpMan::m_req
pointer also makes it quite trivial to get an argument by name and builds nicely on this pull. I quickly pulled something together in stickies-v@c19d186 just as a PoC that passes functional tests, and (the delta to this PR) seems significantly less work than #27788 (but I've not properly tested/dug into this, just something I thought of).
Thanks, edited subject line and description. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK faac216
ACK faac216 |
src/rpc/util.cpp
Outdated
}; \ | ||
return return_code \ | ||
} \ | ||
void force_semicolon() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:675:1: note: in expansion of macro ‘TMPL_INST’
675 | TMPL_INST(nullptr, std::optional<bool>, maybe_arg ? std::optional{maybe_arg->get_bool()} : std::nullopt;);
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:674:1: note: in expansion of macro ‘TMPL_INST’
674 | TMPL_INST(nullptr, std::optional<double>, maybe_arg ? std::optional{maybe_arg->get_real()} : std::nullopt;);
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:676:1: note: in expansion of macro ‘TMPL_INST’
676 | TMPL_INST(nullptr, const std::string*, maybe_arg ? &maybe_arg->get_str() : nullptr;);
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:675:1: note: in expansion of macro ‘TMPL_INST’
675 | TMPL_INST(nullptr, std::optional<bool>, maybe_arg ? std::optional{maybe_arg->get_bool()} : std::nullopt;);
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:679:1: note: in expansion of macro ‘TMPL_INST’
679 | TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:676:1: note: in expansion of macro ‘TMPL_INST’
676 | TMPL_INST(nullptr, const std::string*, maybe_arg ? &maybe_arg->get_str() : nullptr;);
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:680:1: note: in expansion of macro ‘TMPL_INST’
680 | TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:679:1: note: in expansion of macro ‘TMPL_INST’
679 | TMPL_INST(CheckRequiredOrDefault, int, CHECK_NONFATAL(maybe_arg)->getInt<int>(););
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: error: redundant redeclaration of ‘void force_semicolon()’ in same scope [-Werror=redundant-decls]
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:681:1: note: in expansion of macro ‘TMPL_INST’
681 | TMPL_INST(CheckRequiredOrDefault, const std::string&, CHECK_NONFATAL(maybe_arg)->get_str(););
| ^~~~~~~~~
../../../src/rpc/util.cpp:671:10: note: previous declaration of ‘void force_semicolon()’
671 | void force_semicolon()
| ^~~~~~~~~~~~~~~
../../../src/rpc/util.cpp:680:1: note: in expansion of macro ‘TMPL_INST’
680 | TMPL_INST(CheckRequiredOrDefault, uint64_t, CHECK_NONFATAL(maybe_arg)->getInt<uint64_t>(););
| ^~~~~~~~~
cc1plus: all warnings being treated as errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems to work fine without this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could drop it and just not have the semi-colons after the macro invocations too.
Could also change it to void force_semicolon(ret_type dummy)
to avoid the warning.
clang accepts but ignores -Wredundant-decls
, clang-tidy defaults to ignoring the warning in macros (yay!), but seems like the current code will be annoying for compiling with gcc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why CI didn't fail? I guess this is #25972
faac216
to
c00000d
Compare
Force pushed to fix the two nits, should be trivial to re-ACK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-ACK c00000d
Verified that this:
- updates commit msg to PR title
- implements #28230 (comment)
- fixes #28230 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-ACK c00000d
reACK c00000d |
…ring literals to Parse{Hash,Hex} bb91131 doc: remove out-of-date external link in src/util/strencodings.h (Jon Atack) 7d494a4 refactor: use string_view to pass string literals to Parse{Hash,Hex} (Jon Atack) Pull request description: as `string_view` is optimized to be trivially copiable, whereas the current code creates a `std::string` copy at each call. These utility methods are called by quite a few RPCs and tests, as well as by each other. ``` $ git grep "ParseHashV\|ParseHashO\|ParseHexV\|ParseHexO" | wc -l 61 ``` Also remove an out-of-date external link. ACKs for top commit: jonatack: Rebased per `git range-diff c9273f6 b94581a bb91131` for an include header from the merge of bitcoin/bitcoin#28230. Should be trivial to re-ACK. maflcko: lgtm ACK bb91131 ns-xvrn: ACK bitcoin/bitcoin@bb91131 achow101: ACK bb91131 brunoerg: crACK bb91131 Tree-SHA512: 9734fe022c9e43fd93c23a917770d332dbbd3132c80a234059714c32faa6469391e59349954749fc86c4ef0b18d5fd99bf8f4b7b82d9f799943799c1253272ae
Currently the RPC method implementations have many issues:
if
.Fix all issues by adding default helper that can be called via
self.Arg<int>(0)
. The helper needs a few lines of code in thesrc/rpc/util.h
header file. Everything else will be implemented in the cpp file once and if an RPC method needs it.There is also an
self.MaybeArg<int>(0)
helper that works on any arg to return the argument, the default, or a falsy value.