Skip to content

Commit 3dea1d5

Browse files
committed
Merge #135: refactor: proxy-types.h API cleanup
7d59b8d move: add mp/type-data.h (Ryan Ofsky) 798f4b5 moveonly: add mp/type-chrono.h (Ryan Ofsky) a595a0b moveonly: add mp/type-threadmap.h (Ryan Ofsky) e834ebd moveonly: add mp/type-decay.h (Ryan Ofsky) 5ee6cd4 moveonly: add mp/type-exception.h (Ryan Ofsky) 3946512 moveonly: add mp/type-void.h (Ryan Ofsky) 8969d5a moveonly: add mp/type-message.h (Ryan Ofsky) 11b418f moveonly: add mp/type-struct.h (Ryan Ofsky) 5df55a3 moveonly: add mp/type-function.h (Ryan Ofsky) 0d2f939 moveonly: add mp/type-interface.h (Ryan Ofsky) 5417716 moveonly: add mp/type-char.h (Ryan Ofsky) 83c444d moveonly: add mp/type-string.h (Ryan Ofsky) df1375b moveonly: add mp/type-number.h (Ryan Ofsky) 6d831eb moveonly: add mp/type-tuple.h (Ryan Ofsky) c999100 moveonly: add mp/type-pair.h (Ryan Ofsky) 079277f moveonly: add mp/type-map.h (Ryan Ofsky) 6a68472 moveonly: add mp/type-set.h (Ryan Ofsky) c6246c9 moveonly: add mp/type-vector.h (Ryan Ofsky) 619d2c7 moveonly: add mp/type-pointer.h (Ryan Ofsky) 3cb9d9f moveonly: add mp/type-optional.h (Ryan Ofsky) b32e2b0 moveonly: add mp/type-context.h (Ryan Ofsky) f18a1cc refactor: Rename ReadDestValue to ReadDestUpdate (Ryan Ofsky) Pull request description: Goal of this PR is to resolve #50 by cleaning up the sprawling [`proxy-types.h`](https://github.com/chaincodelabs/libmultiprocess/blob/master/include/mp/proxy-types.h) code and organizing all the `CustomReadField` / `CustomBuildField` overloads so they are easier to locate and compare. This PR also resolves #122 by pairing the existing `CustomBuildField` `::capnp::Data` overload with a new `CustomReadField` `::capnp::Data` overload, and should make it easier to see when build & read overloads are inconsistent in the future and prevent that type of bug from happening in the future. This change is an API change that requires some changes to downstream bitcoin core code, so I will push another PR updating that code and link to it here. Top commit has no ACKs. Tree-SHA512: 96b59e752ea560fd5d4fab181dec5b478ea9da4572a3ae1eb5a7d1f0fc1b2e9b63d50db39b67a2a98d0578fb7ddc4b3069472b59deef7a6cf4d73b7328ad7678
2 parents 10bb7e4 + 7d59b8d commit 3dea1d5

28 files changed

+1342
-977
lines changed

CMakeLists.txt

+21
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ set(MP_PUBLIC_HEADERS
4545
include/mp/proxy-io.h
4646
include/mp/proxy-types.h
4747
include/mp/proxy.h
48+
include/mp/type-char.h
49+
include/mp/type-chrono.h
50+
include/mp/type-context.h
51+
include/mp/type-data.h
52+
include/mp/type-decay.h
53+
include/mp/type-exception.h
54+
include/mp/type-function.h
55+
include/mp/type-interface.h
56+
include/mp/type-map.h
57+
include/mp/type-message.h
58+
include/mp/type-number.h
59+
include/mp/type-optional.h
60+
include/mp/type-pair.h
61+
include/mp/type-pointer.h
62+
include/mp/type-set.h
63+
include/mp/type-string.h
64+
include/mp/type-struct.h
65+
include/mp/type-threadmap.h
66+
include/mp/type-tuple.h
67+
include/mp/type-vector.h
68+
include/mp/type-void.h
4869
include/mp/util.h)
4970
add_library(multiprocess STATIC
5071
${MP_PROXY_SRCS}

example/calculator.capnp

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ using Cxx = import "/capnp/c++.capnp";
88
using Proxy = import "/mp/proxy.capnp";
99

1010
$Proxy.include("calculator.h");
11+
$Proxy.includeTypes("types.h");
1112

1213
interface CalculatorInterface $Proxy.wrap("Calculator") {
1314
destroy @0 (context :Proxy.Context) -> ();

example/printer.capnp

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ using Cxx = import "/capnp/c++.capnp";
88
using Proxy = import "/mp/proxy.capnp";
99

1010
$Proxy.include("printer.h");
11+
$Proxy.includeTypes("types.h");
1112

1213
interface PrinterInterface $Proxy.wrap("Printer") {
1314
destroy @0 (context :Proxy.Context) -> ();

example/types.h

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef EXAMPLE_TYPES_H
6+
#define EXAMPLE_TYPES_H
7+
8+
#include <mp/type-context.h>
9+
#include <mp/type-decay.h>
10+
#include <mp/type-interface.h>
11+
#include <mp/type-string.h>
12+
#include <mp/type-threadmap.h>
13+
14+
#endif // EXAMPLE_TYPES_H

include/mp/proxy-types.h

+4-977
Large diffs are not rendered by default.

include/mp/type-char.h

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef MP_PROXY_TYPE_CHAR_H
6+
#define MP_PROXY_TYPE_CHAR_H
7+
8+
#include <mp/util.h>
9+
10+
namespace mp {
11+
template <typename Output, size_t size>
12+
void CustomBuildField(TypeList<const unsigned char*>,
13+
Priority<3>,
14+
InvokeContext& invoke_context,
15+
const unsigned char (&value)[size],
16+
Output&& output)
17+
{
18+
auto result = output.init(size);
19+
memcpy(result.begin(), value, size);
20+
}
21+
22+
template <size_t size, typename Input, typename ReadDest>
23+
decltype(auto) CustomReadField(TypeList<unsigned char[size]>,
24+
Priority<1>,
25+
InvokeContext& invoke_context,
26+
Input&& input,
27+
ReadDest&& read_dest)
28+
{
29+
return read_dest.update([&](auto& value) {
30+
auto data = input.get();
31+
memcpy(value, data.begin(), size);
32+
});
33+
}
34+
} // namespace mp
35+
36+
#endif // MP_PROXY_TYPE_CHAR_H

include/mp/type-chrono.h

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef MP_PROXY_TYPE_CHRONO_H
6+
#define MP_PROXY_TYPE_CHRONO_H
7+
8+
#include <mp/util.h>
9+
10+
#include <chrono>
11+
12+
namespace mp {
13+
//! Overload CustomBuildField and CustomReadField to serialize std::chrono
14+
//! parameters and return values as numbers.
15+
template <class Rep, class Period, typename Value, typename Output>
16+
void CustomBuildField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context, Value&& value,
17+
Output&& output)
18+
{
19+
static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(),
20+
"capnp type does not have enough range to hold lowest std::chrono::duration value");
21+
static_assert(std::numeric_limits<decltype(output.get())>::max() >= std::numeric_limits<Rep>::max(),
22+
"capnp type does not have enough range to hold highest std::chrono::duration value");
23+
output.set(value.count());
24+
}
25+
26+
template <class Rep, class Period, typename Input, typename ReadDest>
27+
decltype(auto) CustomReadField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context,
28+
Input&& input, ReadDest&& read_dest)
29+
{
30+
return read_dest.construct(input.get());
31+
}
32+
} // namespace mp
33+
34+
#endif // MP_PROXY_TYPE_CHRONO_H

include/mp/type-context.h

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef MP_PROXY_TYPE_CONTEXT_H
6+
#define MP_PROXY_TYPE_CONTEXT_H
7+
8+
#include <mp/proxy-io.h>
9+
#include <mp/util.h>
10+
11+
namespace mp {
12+
template <typename Output>
13+
void CustomBuildField(TypeList<>,
14+
Priority<1>,
15+
ClientInvokeContext& invoke_context,
16+
Output&& output,
17+
typename std::enable_if<std::is_same<decltype(output.get()), Context::Builder>::value>::type* enable = nullptr)
18+
{
19+
auto& connection = invoke_context.connection;
20+
auto& thread_context = invoke_context.thread_context;
21+
22+
// Create local Thread::Server object corresponding to the current thread
23+
// and pass a Thread::Client reference to it in the Context.callbackThread
24+
// field so the function being called can make callbacks to this thread.
25+
// Also store the Thread::Client reference in the callback_threads map so
26+
// future calls over this connection can reuse it.
27+
auto [callback_thread, _]{SetThread(
28+
thread_context.callback_threads, thread_context.waiter->m_mutex, &connection,
29+
[&] { return connection.m_threads.add(kj::heap<ProxyServer<Thread>>(thread_context, std::thread{})); })};
30+
31+
// Call remote ThreadMap.makeThread function so server will create a
32+
// dedicated worker thread to run function calls from this thread. Store the
33+
// Thread::Client reference it returns in the request_threads map.
34+
auto make_request_thread{[&]{
35+
// This code will only run if an IPC client call is being made for the
36+
// first time on this thread. After the first call, subsequent calls
37+
// will use the existing request thread. This code will also never run at
38+
// all if the current thread is a request thread created for a different
39+
// IPC client, because in that case PassField code (below) will have set
40+
// request_thread to point to the calling thread.
41+
auto request = connection.m_thread_map.makeThreadRequest();
42+
request.setName(thread_context.thread_name);
43+
return request.send().getResult(); // Nonblocking due to capnp request pipelining.
44+
}};
45+
auto [request_thread, _1]{SetThread(
46+
thread_context.request_threads, thread_context.waiter->m_mutex,
47+
&connection, make_request_thread)};
48+
49+
auto context = output.init();
50+
context.setThread(request_thread->second.m_client);
51+
context.setCallbackThread(callback_thread->second.m_client);
52+
}
53+
54+
//! PassField override for mp.Context arguments. Return asynchronously and call
55+
//! function on other thread found in context.
56+
template <typename Accessor, typename ServerContext, typename Fn, typename... Args>
57+
auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn& fn, Args&&... args) ->
58+
typename std::enable_if<
59+
std::is_same<decltype(Accessor::get(server_context.call_context.getParams())), Context::Reader>::value,
60+
kj::Promise<typename ServerContext::CallContext>>::type
61+
{
62+
const auto& params = server_context.call_context.getParams();
63+
Context::Reader context_arg = Accessor::get(params);
64+
auto future = kj::newPromiseAndFulfiller<typename ServerContext::CallContext>();
65+
auto& server = server_context.proxy_server;
66+
int req = server_context.req;
67+
auto invoke = MakeAsyncCallable(
68+
[fulfiller = kj::mv(future.fulfiller),
69+
call_context = kj::mv(server_context.call_context), &server, req, fn, args...]() mutable {
70+
const auto& params = call_context.getParams();
71+
Context::Reader context_arg = Accessor::get(params);
72+
ServerContext server_context{server, call_context, req};
73+
bool disconnected{false};
74+
{
75+
// Before invoking the function, store a reference to the
76+
// callbackThread provided by the client in the
77+
// thread_local.request_threads map. This way, if this
78+
// server thread needs to execute any RPCs that call back to
79+
// the client, they will happen on the same client thread
80+
// that is waiting for this function, just like what would
81+
// happen if this were a normal function call made on the
82+
// local stack.
83+
//
84+
// If the request_threads map already has an entry for this
85+
// connection, it will be left unchanged, and it indicates
86+
// that the current thread is an RPC client thread which is
87+
// in the middle of an RPC call, and the current RPC call is
88+
// a nested call from the remote thread handling that RPC
89+
// call. In this case, the callbackThread value should point
90+
// to the same thread already in the map, so there is no
91+
// need to update the map.
92+
auto& thread_context = g_thread_context;
93+
auto& request_threads = thread_context.request_threads;
94+
auto [request_thread, inserted]{SetThread(
95+
request_threads, thread_context.waiter->m_mutex,
96+
server.m_context.connection,
97+
[&] { return context_arg.getCallbackThread(); })};
98+
99+
// If an entry was inserted into the requests_threads map,
100+
// remove it after calling fn.invoke. If an entry was not
101+
// inserted, one already existed, meaning this must be a
102+
// recursive call (IPC call calling back to the caller which
103+
// makes another IPC call), so avoid modifying the map.
104+
const bool erase_thread{inserted};
105+
KJ_DEFER({
106+
std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
107+
// Call erase here with a Connection* argument instead
108+
// of an iterator argument, because the `request_thread`
109+
// iterator may be invalid if the connection is closed
110+
// during this function call. More specifically, the
111+
// iterator may be invalid because SetThread adds a
112+
// cleanup callback to the Connection destructor that
113+
// erases the thread from the map, and also because the
114+
// ProxyServer<Thread> destructor calls
115+
// request_threads.clear().
116+
if (erase_thread) {
117+
disconnected = !request_threads.erase(server.m_context.connection);
118+
} else {
119+
disconnected = !request_threads.count(server.m_context.connection);
120+
}
121+
});
122+
fn.invoke(server_context, args...);
123+
}
124+
if (disconnected) {
125+
// If disconnected is true, the Connection object was
126+
// destroyed during the method call. Deal with this by
127+
// returning without ever fulfilling the promise, which will
128+
// cause the ProxyServer object to leak. This is not ideal,
129+
// but fixing the leak will require nontrivial code changes
130+
// because there is a lot of code assuming ProxyServer
131+
// objects are destroyed before Connection objects.
132+
return;
133+
}
134+
KJ_IF_MAYBE(exception, kj::runCatchingExceptions([&]() {
135+
server.m_context.connection->m_loop.sync([&] {
136+
auto fulfiller_dispose = kj::mv(fulfiller);
137+
fulfiller_dispose->fulfill(kj::mv(call_context));
138+
});
139+
}))
140+
{
141+
server.m_context.connection->m_loop.sync([&]() {
142+
auto fulfiller_dispose = kj::mv(fulfiller);
143+
fulfiller_dispose->reject(kj::mv(*exception));
144+
});
145+
}
146+
});
147+
148+
// Lookup Thread object specified by the client. The specified thread should
149+
// be a local Thread::Server object, but it needs to be looked up
150+
// asynchronously with getLocalServer().
151+
auto thread_client = context_arg.getThread();
152+
return server.m_context.connection->m_threads.getLocalServer(thread_client)
153+
.then([&server, invoke, req](const kj::Maybe<Thread::Server&>& perhaps) {
154+
// Assuming the thread object is found, pass it a pointer to the
155+
// `invoke` lambda above which will invoke the function on that
156+
// thread.
157+
KJ_IF_MAYBE (thread_server, perhaps) {
158+
const auto& thread = static_cast<ProxyServer<Thread>&>(*thread_server);
159+
server.m_context.connection->m_loop.log()
160+
<< "IPC server post request #" << req << " {" << thread.m_thread_context.thread_name << "}";
161+
thread.m_thread_context.waiter->post(std::move(invoke));
162+
} else {
163+
server.m_context.connection->m_loop.log()
164+
<< "IPC server error request #" << req << ", missing thread to execute request";
165+
throw std::runtime_error("invalid thread handle");
166+
}
167+
})
168+
// Wait for the invocation to finish before returning to the caller.
169+
.then([invoke_wait = kj::mv(future.promise)]() mutable { return kj::mv(invoke_wait); });
170+
}
171+
} // namespace mp
172+
173+
#endif // MP_PROXY_TYPE_CONTEXT_H

include/mp/type-data.h

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef MP_PROXY_TYPE_DATA_H
6+
#define MP_PROXY_TYPE_DATA_H
7+
8+
#include <mp/util.h>
9+
10+
namespace mp {
11+
template <typename T, typename U>
12+
concept IsSpanOf =
13+
std::convertible_to<T, std::span<const U>> &&
14+
std::constructible_from<T, const U*, const U*>;
15+
16+
template <typename T>
17+
concept IsByteSpan =
18+
IsSpanOf<T, std::byte> ||
19+
IsSpanOf<T, char> ||
20+
IsSpanOf<T, unsigned char> ||
21+
IsSpanOf<T, signed char>;
22+
23+
//! Generic ::capnp::Data field builder for any C++ type that can be converted
24+
//! to a span of bytes, like std::vector<char> or std::array<uint8_t>, or custom
25+
//! blob types like uint256 or PKHash with data() and size() methods pointing to
26+
//! bytes.
27+
template <typename LocalType, typename Value, typename Output>
28+
void CustomBuildField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Value&& value, Output&& output)
29+
requires (std::is_same_v<decltype(output.get()), ::capnp::Data::Builder> && IsByteSpan<LocalType>)
30+
{
31+
auto data = std::span{value};
32+
auto result = output.init(data.size());
33+
memcpy(result.begin(), data.data(), data.size());
34+
}
35+
36+
template <typename LocalType, typename Input, typename ReadDest>
37+
decltype(auto) CustomReadField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest)
38+
requires (std::is_same_v<decltype(input.get()), ::capnp::Data::Reader> && IsByteSpan<LocalType>)
39+
{
40+
using ByteType = decltype(std::span{std::declval<LocalType>().begin(), std::declval<LocalType>().end()})::element_type;
41+
const kj::byte *begin{input.get().begin()}, *end{input.get().end()};
42+
return read_dest.construct(reinterpret_cast<const ByteType*>(begin), reinterpret_cast<const ByteType*>(end));
43+
}
44+
} // namespace mp
45+
46+
#endif // MP_PROXY_TYPE_DATA_H

include/mp/type-decay.h

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef MP_PROXY_TYPE_DECAY_H
6+
#define MP_PROXY_TYPE_DECAY_H
7+
8+
#include <mp/util.h>
9+
10+
namespace mp {
11+
template <typename LocalType, typename Value, typename Output>
12+
void CustomBuildField(TypeList<const LocalType>,
13+
Priority<0>,
14+
InvokeContext& invoke_context,
15+
Value&& value,
16+
Output&& output)
17+
{
18+
BuildField(TypeList<LocalType>(), invoke_context, output, std::forward<Value>(value));
19+
}
20+
21+
template <typename LocalType, typename Value, typename Output>
22+
void CustomBuildField(TypeList<LocalType&>, Priority<0>, InvokeContext& invoke_context, Value&& value, Output&& output)
23+
{
24+
BuildField(TypeList<LocalType>(), invoke_context, output, std::forward<Value>(value));
25+
}
26+
27+
template <typename LocalType, typename Value, typename Output>
28+
void CustomBuildField(TypeList<LocalType&&>,
29+
Priority<0>,
30+
InvokeContext& invoke_context,
31+
Value&& value,
32+
Output&& output)
33+
{
34+
BuildField(TypeList<LocalType>(), invoke_context, output, std::forward<Value>(value));
35+
}
36+
} // namespace mp
37+
38+
#endif // MP_PROXY_TYPE_DECAY_H

0 commit comments

Comments
 (0)