Skip to content

Commit fb2622f

Browse files
Sjorsryanofsky
andcommitted
ipc: Serialize null CTransactionRef as empty Data
Reference and rationale: bitcoin/bitcoin#34020 (comment) Empty `Data` can be a valid representation for some types, so this behavior should not be generalized. In `List(Data)` contexts, Cap'n Proto's current C++ API does not let us distinguish null vs empty `Data`. Given those constraints, this commit is narrowly scoped: interpret empty `Data` as null only for `CTransactionRef`-like pointer paths where empty serialization is known to be safe. The implementation avoids making generic pointer-type assumptions and introduces a targeted has-field customization path. Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
1 parent 3a69d47 commit fb2622f

File tree

6 files changed

+66
-16
lines changed

6 files changed

+66
-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 represeent 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
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 contexts, 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-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
}

0 commit comments

Comments
 (0)