Skip to content

Commit 8a5e3ae

Browse files
committed
Merge #242: proxy-types: add CustomHasField hook to map Cap'n Proto values to null C++ values
97d8770 proxy-types: add CustomHasField hook for nullable decode paths (Sjors Provoost) 8c2f102 refactor: add missing includes to mp/type-data.h (Sjors Provoost) Pull request description: Taken from: bitcoin/bitcoin#34020 (comment) Let applications override `CustomHasField` so they can decide to treat certain capnproto values as being unset. For example, when converting `List(Data)` to `vector<shared_ptr<CTransaction>>`, mapping empty `Data` fields to null pointers. This safe to do in special cases, like in this example because serialized `CTransaction` representations are never empty. It is also useful to do in this case because Cap'n Proto doesn't currently provide any API for distinguishing between unset and empty data values in a list (although they can be distinguished on the wire). ACKs for top commit: ryanofsky: Code review ACK 97d8770. Confirmed no changes since last rebase. Tree-SHA512: 34a9e9961a6e9334f21c684d9c89be98c85b6269852a6fc169ffdec47efb2a42b1e76e6d52090f448ed1a56d6995a8dde74d2c6a6fec75ca46bfd551a26e54cf
2 parents e8d3524 + 97d8770 commit 8a5e3ae

File tree

12 files changed

+94
-16
lines changed

12 files changed

+94
-16
lines changed

include/mp/proxy-types.h

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ class ValueField
2323
ValueField(Value&& value) : m_value(value) {}
2424
Value& m_value;
2525

26+
const Value& get() const { return m_value; }
2627
Value& get() { return m_value; }
2728
Value& init() { return m_value; }
28-
bool has() { return true; }
29+
bool has() const { return true; }
2930
};
3031

3132
template <typename Accessor, typename Struct>
@@ -166,10 +167,52 @@ struct ReadDestUpdate
166167
Value& m_value;
167168
};
168169

169-
template <typename... LocalTypes, typename... Args>
170-
decltype(auto) ReadField(TypeList<LocalTypes...>, Args&&... args)
170+
//! Return whether to read a C++ value from a Cap'n Proto field. Returning
171+
//! false can be useful to interpret certain Cap'n Proto field values as null
172+
//! C++ values when initializing nullable C++ std::optional / std::unique_ptr /
173+
//! std::shared_ptr types.
174+
//!
175+
//! For example, when reading from a `List(Data)` field into a
176+
//! `std::vector<std::shared_ptr<const CTransaction>>` value, it's useful to be
177+
//! able to interpret empty `Data` values as null pointers. This is useful
178+
//! because the Cap'n Proto C++ API does not currently provide a way to
179+
//! distinguish between null and empty Data values in a List[*], so we need to
180+
//! choose some Data value to represent null if we want to allow passing null
181+
//! pointers. Since no CTransaction is ever serialized as empty Data, it's safe
182+
//! to use empty Data values to represent null pointers.
183+
//!
184+
//! [*] The Cap'n Proto wire format actually does distinguish between null and
185+
//! empty Data values inside Lists, and the C++ API does allow distinguishing
186+
//! between null and empty Data values in other contexts, just not the List
187+
//! context, so this limitation could be removed in the future.
188+
//!
189+
//! Design note: CustomHasField() and CustomHasValue() are inverses of each
190+
//! other. CustomHasField() allows leaving Cap'n Proto fields unset when C++
191+
//! types have certain values, and CustomHasValue() allows leaving C++ values
192+
//! unset when Cap'n Proto fields have certain values. But internally the
193+
//! functions get called in different ways. This is because in C++, unlike in
194+
//! Cap'n Proto not every C++ type is default constructible, and it may be
195+
//! impossible to leave certain C++ values unset. For example if a C++ method
196+
//! requires function parameters, there's no way to call the function without
197+
//! constructing values for each of the parameters. Similarly there's no way to
198+
//! add values to C++ vectors or maps without initializing those values. This
199+
//! is not the case in Cap'n Proto where all values are optional and it's
200+
//! possible to skip initializing parameters and list elements.
201+
//!
202+
//! Because of this difference, CustomHasValue() works universally and can be
203+
//! used to disable BuildField() calls in every context, while CustomHasField()
204+
//! can only be used to disable ReadField() calls in certain contexts like
205+
//! std::optional and pointer contexts.
206+
template <typename... LocalTypes, typename Input>
207+
bool CustomHasField(TypeList<LocalTypes...>, InvokeContext& invoke_context, const Input& input)
208+
{
209+
return input.has();
210+
}
211+
212+
template <typename... LocalTypes, typename Input, typename... Args>
213+
decltype(auto) ReadField(TypeList<LocalTypes...>, InvokeContext& invoke_context, Input&& input, Args&&... args)
171214
{
172-
return CustomReadField(TypeList<RemoveCvRef<LocalTypes>...>(), Priority<2>(), std::forward<Args>(args)...);
215+
return CustomReadField(TypeList<RemoveCvRef<LocalTypes>...>(), Priority<2>(), invoke_context, std::forward<Input>(input), std::forward<Args>(args)...);
173216
}
174217

175218
template <typename LocalType, typename Input>
@@ -190,6 +233,13 @@ void ThrowField(TypeList<std::exception>, InvokeContext& invoke_context, Input&&
190233
throw std::runtime_error(std::string(CharCast(data.begin()), data.size()));
191234
}
192235

236+
//! Return whether to write a C++ value into a Cap'n Proto field. Returning
237+
//! false can be useful to map certain C++ values to unset Cap'n Proto fields.
238+
//!
239+
//! For example the bitcoin `Coin` class asserts false when a spent coin is
240+
//! serialized. But some C++ methods return these coins, so there needs to be a
241+
//! way to represent them in Cap'n Proto and a null Data field is a convenient
242+
//! representation.
193243
template <typename... Values>
194244
bool CustomHasValue(InvokeContext& invoke_context, const Values&... value)
195245
{
@@ -372,7 +422,7 @@ struct ClientException
372422
void handleField(InvokeContext& invoke_context, Results& results, ParamList)
373423
{
374424
StructField<Accessor, Results> input(results);
375-
if (input.has()) {
425+
if (CustomHasField(TypeList<Exception>(), invoke_context, input)) {
376426
ThrowField(TypeList<Exception>(), invoke_context, input);
377427
}
378428
}

include/mp/type-data.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
#include <mp/util.h>
99

10+
#include <concepts>
11+
#include <span>
12+
1013
namespace mp {
1114
template <typename T, typename U>
1215
concept IsSpanOf =

include/mp/type-function.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ struct ProxyCallFn
4848
};
4949

5050
template <typename FnR, typename... FnParams, typename Input, typename ReadDest>
51-
decltype(auto) CustomReadField(TypeList<std::function<FnR(FnParams...)>>,
51+
decltype(auto) CustomReadField(TypeList<std::function<FnR(FnParams...)>> types,
5252
Priority<1>,
5353
InvokeContext& invoke_context,
5454
Input&& input,
5555
ReadDest&& read_dest)
5656
{
57-
if (input.has()) {
57+
if (CustomHasField(types, invoke_context, input)) {
5858
using Interface = typename Decay<decltype(input.get())>::Calls;
5959
auto client = std::make_shared<ProxyClient<Interface>>(
6060
input.get(), &invoke_context.connection, /* destroy_connection= */ false);

include/mp/type-interface.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ decltype(auto) CustomReadField(TypeList<std::unique_ptr<LocalType>>,
8585
typename Decay<decltype(input.get())>::Calls* enable = nullptr)
8686
{
8787
using Interface = typename Decay<decltype(input.get())>::Calls;
88-
if (input.has()) {
88+
if (CustomHasField(TypeList<LocalType>(), invoke_context, input)) {
8989
return read_dest.construct(
9090
CustomMakeProxyClient<Interface, LocalType>(invoke_context, std::move(input.get())));
9191
}
@@ -101,7 +101,7 @@ decltype(auto) CustomReadField(TypeList<std::shared_ptr<LocalType>>,
101101
typename Decay<decltype(input.get())>::Calls* enable = nullptr)
102102
{
103103
using Interface = typename Decay<decltype(input.get())>::Calls;
104-
if (input.has()) {
104+
if (CustomHasField(TypeList<LocalType>(), invoke_context, input)) {
105105
return read_dest.construct(
106106
CustomMakeProxyClient<Interface, LocalType>(invoke_context, std::move(input.get())));
107107
}

include/mp/type-message.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ void CustomBuildField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_co
2323
//! overloads. Defining a CustomReadMessage overload is simpler than defining a
2424
//! CustomReadField overload because it only requires defining a normal
2525
//! function, not a template function, but less flexible.
26-
template <typename LocalType, typename Reader, typename ReadDest>
27-
decltype(auto) CustomReadField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Reader&& reader,
26+
template <typename LocalType, typename Input, typename ReadDest>
27+
decltype(auto) CustomReadField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Input&& input,
2828
ReadDest&& read_dest,
29-
decltype(CustomReadMessage(invoke_context, reader.get(),
29+
decltype(CustomReadMessage(invoke_context, input.get(),
3030
std::declval<LocalType&>()))* enable = nullptr)
3131
{
32-
return read_dest.update([&](auto& value) { if (reader.has()) CustomReadMessage(invoke_context, reader.get(), value); });
32+
return read_dest.update([&](auto& value) { if (CustomHasField(TypeList<LocalType>(), invoke_context, input)) CustomReadMessage(invoke_context, input.get(), value); });
3333
}
3434

3535
//! Helper for CustomPassField below. Call Accessor::init method if it has one,

include/mp/type-optional.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ decltype(auto) CustomReadField(TypeList<std::optional<LocalType>>,
3030
ReadDest&& read_dest)
3131
{
3232
return read_dest.update([&](auto& value) {
33-
if (!input.has()) {
33+
if (!CustomHasField(TypeList<LocalType>(), invoke_context, input)) {
3434
value.reset();
3535
} else if (value) {
3636
ReadField(TypeList<LocalType>(), invoke_context, input, ReadDestUpdate(*value));

include/mp/type-pointer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ decltype(auto) CustomReadField(TypeList<std::shared_ptr<LocalType>>,
5050
ReadDest&& read_dest)
5151
{
5252
return read_dest.update([&](auto& value) {
53-
if (!input.has()) {
53+
if (!CustomHasField(TypeList<LocalType>(), invoke_context, input)) {
5454
value.reset();
5555
} else if (value) {
5656
ReadField(TypeList<LocalType>(), invoke_context, input, ReadDestUpdate(*value));
@@ -72,7 +72,7 @@ decltype(auto) CustomReadField(TypeList<std::shared_ptr<const LocalType>>,
7272
ReadDest&& read_dest)
7373
{
7474
return read_dest.update([&](auto& value) {
75-
if (!input.has()) {
75+
if (!CustomHasField(TypeList<LocalType>(), invoke_context, input)) {
7676
value.reset();
7777
return;
7878
}

src/mp/gen.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ static void Generate(kj::StringPtr src_prefix,
228228
cpp_client << "#include <" << include_path << ".proxy-types.h>\n";
229229
cpp_client << "#include <capnp/generated-header-support.h>\n";
230230
cpp_client << "#include <cstring>\n";
231+
cpp_client << "#include <vector>\n";
231232
cpp_client << "#include <kj/common.h>\n";
232233
cpp_client << "#include <mp/proxy.h>\n";
233234
cpp_client << "#include <mp/util.h>\n";

test/mp/test/foo-types.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
#include <cstddef>
1414
#include <mp/test/foo.capnp.h>
1515
#include <mp/type-context.h>
16+
#include <mp/type-data.h>
1617
#include <mp/type-decay.h>
1718
#include <mp/type-function.h>
1819
#include <mp/type-interface.h>
1920
#include <mp/type-map.h>
2021
#include <mp/type-message.h>
2122
#include <mp/type-number.h>
23+
#include <mp/type-pointer.h>
2224
#include <mp/type-set.h>
2325
#include <mp/type-string.h>
2426
#include <mp/type-struct.h>
@@ -56,6 +58,14 @@ decltype(auto) CustomReadField(TypeList<FooCustom>, Priority<1>, InvokeContext&
5658

5759
} // namespace test
5860

61+
template <typename Input>
62+
bool CustomHasField(TypeList<test::FooData>, InvokeContext& invoke_context, const Input& input)
63+
{
64+
// Cap'n Proto C++ cannot distinguish null vs empty Data in List(Data), so
65+
// interpret empty Data as null for this specific type.
66+
return input.get().size() != 0;
67+
}
68+
5969
inline void CustomBuildMessage(InvokeContext& invoke_context,
6070
const test::FooMessage& src,
6171
test::messages::FooMessage::Builder&& builder)

test/mp/test/foo.capnp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
3434
callFn @17 () -> ();
3535
callFnAsync @18 (context :Proxy.Context) -> ();
3636
callIntFnAsync @21 (context :Proxy.Context, arg :Int32) -> (result :Int32);
37+
passDataPointers @22 (arg :List(Data)) -> (result :List(Data));
3738
}
3839

3940
interface FooCallback $Proxy.wrap("mp::test::FooCallback") {

0 commit comments

Comments
 (0)