multiprocess: Add type conversion code for serializable types

Allow any C++ object that has Serialize and Unserialize methods and can be
serialized to a bitcoin CDataStream to be converted to a capnproto Data field
and passed as arguments or return values to capnproto methods using the Data
type.

Extend IPC unit test to cover this and verify the serialization happens
correctly.
This commit is contained in:
Ryan Ofsky 2023-11-20 15:49:55 -05:00
parent 4aaee23921
commit 0cc74fce72
5 changed files with 98 additions and 1 deletions

View file

@ -1096,6 +1096,7 @@ ipc/capnp/libbitcoin_ipc_a-protocol.$(OBJEXT): $(libbitcoin_ipc_mpgen_input:=.h)
if BUILD_MULTIPROCESS
LIBBITCOIN_IPC=libbitcoin_ipc.a
libbitcoin_ipc_a_SOURCES = \
ipc/capnp/common-types.h \
ipc/capnp/context.h \
ipc/capnp/init-types.h \
ipc/capnp/protocol.cpp \

View file

@ -0,0 +1,89 @@
// Copyright (c) 2023 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_IPC_CAPNP_COMMON_TYPES_H
#define BITCOIN_IPC_CAPNP_COMMON_TYPES_H
#include <clientversion.h>
#include <streams.h>
#include <cstddef>
#include <mp/proxy-types.h>
#include <type_traits>
#include <utility>
namespace ipc {
namespace capnp {
//! Use SFINAE to define Serializeable<T> trait which is true if type T has a
//! Serialize(stream) method, false otherwise.
template <typename T>
struct Serializable {
private:
template <typename C>
static std::true_type test(decltype(std::declval<C>().Serialize(std::declval<std::nullptr_t&>()))*);
template <typename>
static std::false_type test(...);
public:
static constexpr bool value = decltype(test<T>(nullptr))::value;
};
//! Use SFINAE to define Unserializeable<T> trait which is true if type T has
//! an Unserialize(stream) method, false otherwise.
template <typename T>
struct Unserializable {
private:
template <typename C>
static std::true_type test(decltype(std::declval<C>().Unserialize(std::declval<std::nullptr_t&>()))*);
template <typename>
static std::false_type test(...);
public:
static constexpr bool value = decltype(test<T>(nullptr))::value;
};
} // namespace capnp
} // namespace ipc
//! Functions to serialize / deserialize common bitcoin types.
namespace mp {
//! Overload multiprocess library's CustomBuildField hook to allow any
//! serializable object to be stored in a capnproto Data field or passed to a
//! canproto interface. Use Priority<1> so this hook has medium priority, and
//! higher priority hooks could take precedence over this one.
template <typename LocalType, typename Value, typename Output>
void CustomBuildField(
TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output,
// Enable if serializeable and if LocalType is not cv or reference
// qualified. If LocalType is cv or reference qualified, it is important to
// fall back to lower-priority Priority<0> implementation of this function
// that strips cv references, to prevent this CustomBuildField overload from
// taking precedence over more narrow overloads for specific LocalTypes.
std::enable_if_t<ipc::capnp::Serializable<LocalType>::value &&
std::is_same_v<LocalType, std::remove_cv_t<std::remove_reference_t<LocalType>>>>* enable = nullptr)
{
DataStream stream;
value.Serialize(stream);
auto result = output.init(stream.size());
memcpy(result.begin(), stream.data(), stream.size());
}
//! Overload multiprocess library's CustomReadField hook to allow any object
//! with an Unserialize method to be read from a capnproto Data field or
//! returned from canproto interface. Use Priority<1> so this hook has medium
//! priority, and higher priority hooks could take precedence over this one.
template <typename LocalType, typename Input, typename ReadDest>
decltype(auto)
CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest,
std::enable_if_t<ipc::capnp::Unserializable<LocalType>::value>* enable = nullptr)
{
return read_dest.update([&](auto& value) {
if (!input.has()) return;
auto data = input.get();
SpanReader stream({data.begin(), data.end()});
value.Unserialize(stream);
});
}
} // namespace mp
#endif // BITCOIN_IPC_CAPNP_COMMON_TYPES_H

View file

@ -9,7 +9,9 @@ $Cxx.namespace("gen");
using Proxy = import "/mp/proxy.capnp";
$Proxy.include("test/ipc_test.h");
$Proxy.includeTypes("ipc/capnp/common-types.h");
interface FooInterface $Proxy.wrap("FooImplementation") {
add @0 (a :Int32, b :Int32) -> (result :Int32);
passOutPoint @1 (arg :Data) -> (result :Data);
}

View file

@ -2,8 +2,8 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <mp/proxy-types.h>
#include <logging.h>
#include <mp/proxy-types.h>
#include <test/ipc_test.capnp.h>
#include <test/ipc_test.capnp.proxy.h>
#include <test/ipc_test.h>
@ -51,6 +51,10 @@ void IpcTest()
// Test: make sure arguments were sent and return value is received
BOOST_CHECK_EQUAL(foo->add(1, 2), 3);
COutPoint txout1{Txid::FromUint256(uint256{100}), 200};
COutPoint txout2{foo->passOutPoint(txout1)};
BOOST_CHECK(txout1 == txout2);
// Test cleanup: disconnect pipe and join thread
disconnect_client();
thread.join();

View file

@ -11,6 +11,7 @@ class FooImplementation
{
public:
int add(int a, int b) { return a + b; }
COutPoint passOutPoint(COutPoint o) { return o; }
};
void IpcTest();