Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b15d63e
doc: Bump version 11 > 12
ryanofsky Apr 15, 2026
36c91a0
util, refactor: Add ProcessId type alias and use it
ryanofsky Apr 30, 2025
94af41b
util, refactor: Add SocketId type alias and use it
ryanofsky Apr 30, 2025
beaa50a
util, refactor: Add ConnectInfo type alias and use it
ryanofsky Apr 30, 2025
b16f8c4
util, refactor: Handle forking inside ExecProcess
ryanofsky Apr 17, 2026
022b29b
util, refactor: Add SocketPair() and use it in SpawnProcess
ryanofsky Apr 30, 2025
24c5e57
util: Clear FD_CLOEXEC on child socket before exec
ryanofsky Apr 30, 2025
3c81cf2
proxy, refactor: Replace EventLoop wakeup fd integers with KJ stream …
ryanofsky Apr 30, 2025
17a1952
cmake: Bump minimum required Cap'n Proto version to 0.9
ryanofsky Apr 16, 2026
091f5e1
proxy, refactor: Change ConnectStream and ServeStream to accept strea…
ryanofsky Apr 30, 2025
bfc2db7
proxy: Call shutdownWrite() in Connection destructor
ryanofsky Apr 30, 2025
1060a95
util, refactor: Fix PtrOrValue constructor for move-only types on MSVC
ryanofsky Apr 17, 2026
362d416
proxy, refactor: Fix C4305 truncation warning in Accessor on MSVC
ryanofsky Apr 22, 2026
3fd227c
type-interface, refactor: Fix typename decltype() SFINAE in CustomBui…
ryanofsky Apr 22, 2026
926ae35
ci: Check out bitcoin/bitcoin PR #35084 instead of master
ryanofsky Apr 16, 2026
28e4c7f
proxy: Fix shutdownWrite() exception handling on macOS with dynamic l…
ryanofsky Apr 17, 2026
f6aa627
ipc: Wrap mpgen main() in try-catch to print errors
ryanofsky Apr 20, 2026
7f513a4
doc: Remove trailing whitespace
ryanofsky Apr 17, 2026
c9aa806
cmake: Replace capnp_PREFIX path construction with cmake-provided sym…
ryanofsky Apr 21, 2026
7cb83a5
cmake: Fix CapnProto tool paths broken by Ubuntu Noble packaging bug
ryanofsky Apr 22, 2026
4f58c8c
util: Add Windows support
ryanofsky Apr 30, 2025
7fd5ec4
util: drop POSIX/pthread dependencies to enable MSVC builds
ryanofsky Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/bitcoin-core-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ concurrency:

env:
BITCOIN_REPO: bitcoin/bitcoin
# Temporary: use PR #35084 until it merges; revert to refs/heads/master after
BITCOIN_CORE_REF: refs/pull/35084/merge
LLVM_VERSION: 22
LIBCXX_DIR: /tmp/libcxx-build/

Expand Down Expand Up @@ -79,6 +81,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: ${{ env.BITCOIN_REPO }}
ref: ${{ env.BITCOIN_CORE_REF }}
fetch-depth: 1

- name: Checkout libmultiprocess
Expand Down Expand Up @@ -195,6 +198,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: ${{ env.BITCOIN_REPO }}
ref: ${{ env.BITCOIN_CORE_REF }}
fetch-depth: 1

- name: Checkout libmultiprocess
Expand Down
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ endif()
include("cmake/compat_find.cmake")

find_package(Threads REQUIRED)
find_package(CapnProto 0.7 QUIET NO_MODULE)
find_package(CapnProto 0.9 QUIET NO_MODULE)
if(NOT CapnProto_FOUND)
message(FATAL_ERROR
"Cap'n Proto is required but was not found.\n"
Expand Down Expand Up @@ -203,6 +203,10 @@ target_link_libraries(mpgen PRIVATE CapnProto::capnp-rpc)
target_link_libraries(mpgen PRIVATE CapnProto::capnpc)
target_link_libraries(mpgen PRIVATE CapnProto::kj)
target_link_libraries(mpgen PRIVATE Threads::Threads)
target_compile_definitions(mpgen PRIVATE
"CAPNP_EXECUTABLE=\"$<TARGET_FILE:CapnProto::capnp_tool>\""
"CAPNPC_CXX_EXECUTABLE=\"$<TARGET_FILE:CapnProto::capnpc_cpp>\""
"CAPNP_INCLUDE_DIRS=\"${CAPNP_INCLUDE_DIRS}\"")
set_target_properties(mpgen PROPERTIES
INSTALL_RPATH_USE_LINK_PATH TRUE)
set_target_properties(mpgen PROPERTIES
Expand Down
2 changes: 1 addition & 1 deletion ci/configs/olddeps.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CI_DESC="CI job using old Cap'n Proto and cmake versions"
CI_DIR=build-olddeps
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wno-error=array-bounds"
NIX_ARGS=(--argstr capnprotoVersion "0.7.1" --argstr cmakeVersion "3.12.4")
NIX_ARGS=(--argstr capnprotoVersion "0.9.2" --argstr cmakeVersion "3.12.4")
BUILD_ARGS=(-k)
69 changes: 69 additions & 0 deletions cmake/compat_config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,75 @@ if (NOT DEFINED capnp_PREFIX AND DEFINED CAPNP_INCLUDE_DIRS)
get_filename_component(capnp_PREFIX "${CAPNP_INCLUDE_DIRS}" DIRECTORY)
endif()

if (NOT DEFINED CAPNP_INCLUDE_DIRS AND DEFINED capnp_PREFIX)
set(CAPNP_INCLUDE_DIRS "${capnp_PREFIX}/include")
endif()

if (NOT TARGET CapnProto::capnp_tool)
if (DEFINED CAPNP_EXECUTABLE)
add_executable(CapnProto::capnp_tool IMPORTED GLOBAL)
set_target_properties(CapnProto::capnp_tool PROPERTIES IMPORTED_LOCATION "${CAPNP_EXECUTABLE}")
elseif (DEFINED capnp_PREFIX)
add_executable(CapnProto::capnp_tool IMPORTED GLOBAL)
set_target_properties(CapnProto::capnp_tool PROPERTIES IMPORTED_LOCATION "${capnp_PREFIX}/bin/capnp")
endif()
endif()

if (NOT TARGET CapnProto::capnpc_cpp)
if (DEFINED CAPNPC_CXX_EXECUTABLE)
add_executable(CapnProto::capnpc_cpp IMPORTED GLOBAL)
set_target_properties(CapnProto::capnpc_cpp PROPERTIES IMPORTED_LOCATION "${CAPNPC_CXX_EXECUTABLE}")
elseif (DEFINED capnp_PREFIX)
add_executable(CapnProto::capnpc_cpp IMPORTED GLOBAL)
set_target_properties(CapnProto::capnpc_cpp PROPERTIES IMPORTED_LOCATION "${capnp_PREFIX}/bin/capnpc-c++")
endif()
endif()

# Validate CapnProto tool target locations and fix if broken.
# Some packaged capnproto versions (e.g., Ubuntu Noble libcapnp-dev 1.0.1)
# have incorrect IMPORTED_LOCATION paths due to a packaging bug where the cmake
# config file is installed under /usr/lib/.../cmake/ but the _IMPORT_PREFIX
# calculation goes up too few directory levels, yielding /usr/lib/bin/capnp
# instead of the correct /usr/bin/capnp.
foreach(_mp_tool IN ITEMS capnp_tool capnpc_cpp)
if (TARGET "CapnProto::${_mp_tool}")
get_target_property(_mp_configs "CapnProto::${_mp_tool}" IMPORTED_CONFIGURATIONS)
set(_mp_valid FALSE)
foreach(_mp_cfg IN LISTS _mp_configs)
get_target_property(_mp_loc "CapnProto::${_mp_tool}" "IMPORTED_LOCATION_${_mp_cfg}")
if (EXISTS "${_mp_loc}")
set(_mp_valid TRUE)
break()
endif()
endforeach()
if (NOT _mp_valid)
get_target_property(_mp_loc "CapnProto::${_mp_tool}" IMPORTED_LOCATION)
if (EXISTS "${_mp_loc}")
set(_mp_valid TRUE)
endif()
endif()
if (NOT _mp_valid)
if ("${_mp_tool}" STREQUAL "capnp_tool")
find_program(_mp_fixed capnp HINTS "${capnp_PREFIX}/bin")
else()
find_program(_mp_fixed capnpc-c++ HINTS "${capnp_PREFIX}/bin")
endif()
if (_mp_fixed)
foreach(_mp_cfg IN LISTS _mp_configs)
set_target_properties("CapnProto::${_mp_tool}" PROPERTIES "IMPORTED_LOCATION_${_mp_cfg}" "${_mp_fixed}")
endforeach()
set_target_properties("CapnProto::${_mp_tool}" PROPERTIES IMPORTED_LOCATION "${_mp_fixed}")
endif()
unset(_mp_fixed CACHE)
endif()
endif()
endforeach()
unset(_mp_tool)
unset(_mp_configs)
unset(_mp_valid)
unset(_mp_cfg)
unset(_mp_loc)

if (NOT DEFINED CAPNPC_OUTPUT_DIR)
set(CAPNPC_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
endif()
Expand Down
4 changes: 2 additions & 2 deletions doc/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ sequenceDiagram
participant PMT as ProxyMethodTraits
participant Impl as Actual C++ Method

serverInvoke->>SF1: SF1::invoke
serverInvoke->>SF1: SF1::invoke
SF1->>SF2: SF2::invoke
SF2->>SR: SR::invoke
SR->>SC: SC::invoke
Expand Down Expand Up @@ -165,7 +165,7 @@ Thread mapping enables each client thread to have a dedicated server thread proc
Thread mapping is initialized by defining an interface method with a `ThreadMap` parameter and/or response. The example below adds `ThreadMap` to the `construct` method because libmultiprocess calls the `construct` method automatically.

```capnp
interface InitInterface $Proxy.wrap("Init") {
interface InitInterface $Proxy.wrap("Init") {
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
}
```
Expand Down
6 changes: 5 additions & 1 deletion doc/versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ Library versions are tracked with simple
Versioning policy is described in the [version.h](../include/mp/version.h)
include.

## v11
## v12
- Current unstable version.
- Adds support for nonunix platforms, making API changes that are not backwards compatible.

## [v11.0](https://github.com/bitcoin-core/libmultiprocess/commits/v11.0)
- Improves debug output if EventLoop::post callback fails.

## [v10.0](https://github.com/bitcoin-core/libmultiprocess/commits/v10.0)
- Increases spawn test timeout to avoid spurious failures.
Expand Down
13 changes: 4 additions & 9 deletions example/calculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
#include <init.capnp.h>
#include <init.capnp.proxy.h> // NOLINT(misc-include-cleaner) // IWYU pragma: keep

#include <charconv>
#include <cstring>
#include <cstring> // IWYU pragma: keep
#include <fstream>
#include <functional>
#include <iostream>
Expand All @@ -16,9 +15,9 @@
#include <kj/memory.h>
#include <memory>
#include <mp/proxy-io.h>
#include <mp/util.h>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>

class CalculatorImpl : public Calculator
Expand Down Expand Up @@ -51,14 +50,10 @@ int main(int argc, char** argv)
std::cout << "Usage: mpcalculator <fd>\n";
return 1;
}
int fd;
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
std::cerr << argv[1] << " is not a number or is larger than an int\n";
return 1;
}
mp::SocketId socket{mp::StartSpawned(argv[1])};
mp::EventLoop loop("mpcalculator", LogPrint);
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
mp::ServeStream<InitInterface>(loop, fd, *init);
mp::ServeStream<InitInterface>(loop, mp::MakeStream(loop.m_io_context, socket), *init);
loop.loop();
return 0;
}
8 changes: 4 additions & 4 deletions example/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>

namespace fs = std::filesystem;

static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
{
int pid;
const int fd = mp::SpawnProcess(pid, [&](int fd) -> std::vector<std::string> {
const auto [pid, socket] = mp::SpawnProcess([&](mp::ConnectInfo info) -> std::vector<std::string> {
fs::path path = process_argv0;
path.remove_filename();
path.append(new_exe_name);
return {path.string(), std::to_string(fd)};
return {path.string(), std::move(info)};
});
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, fd), pid);
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, mp::MakeStream(loop.m_io_context, socket)), pid);
}

static void LogPrint(mp::LogMessage log_data)
Expand Down
13 changes: 4 additions & 9 deletions example/printer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@
#include <init.capnp.h>
#include <init.capnp.proxy.h> // NOLINT(misc-include-cleaner) // IWYU pragma: keep

#include <charconv>
#include <cstring>
#include <cstring> // IWYU pragma: keep
#include <fstream>
#include <iostream>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <memory>
#include <mp/proxy-io.h>
#include <mp/util.h>
#include <stdexcept>
#include <string>
#include <system_error>

class PrinterImpl : public Printer
{
Expand All @@ -44,14 +43,10 @@ int main(int argc, char** argv)
std::cout << "Usage: mpprinter <fd>\n";
return 1;
}
int fd;
if (std::from_chars(argv[1], argv[1] + strlen(argv[1]), fd).ec != std::errc{}) {
std::cerr << argv[1] << " is not a number or is larger than an int\n";
return 1;
}
mp::SocketId socket{mp::StartSpawned(argv[1])};
mp::EventLoop loop("mpprinter", LogPrint);
std::unique_ptr<Init> init = std::make_unique<InitImpl>();
mp::ServeStream<InitInterface>(loop, fd, *init);
mp::ServeStream<InitInterface>(loop, mp::MakeStream(loop.m_io_context, socket), *init);
loop.loop();
return 0;
}
1 change: 0 additions & 1 deletion include/mp/config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#define MP_CONFIG_H

#cmakedefine CMAKE_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@"
#cmakedefine capnp_PREFIX "@capnp_PREFIX@"
#cmakedefine HAVE_KJ_FILESYSTEM

#cmakedefine HAVE_PTHREAD_GETNAME_NP @HAVE_PTHREAD_GETNAME_NP@
Expand Down
44 changes: 29 additions & 15 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <condition_variable>
#include <functional>
#include <kj/function.h>
#include <kj/io.h>
#include <map>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -210,6 +211,21 @@ class Logger

std::string LongThreadName(const char* exe_name);

inline SocketId StreamSocketId(const Stream& stream)
{
if (stream) KJ_IF_MAYBE(socket, stream->getFd()) return *socket;
#ifdef WIN32
if (stream) KJ_IF_MAYBE(handle, stream->getWin32Handle()) return reinterpret_cast<SocketId>(*handle);
#endif
throw std::logic_error("Stream socket unset");
}

//! Wrap a socket file descriptor as an async stream, taking ownership of the fd.
inline Stream MakeStream(kj::AsyncIoContext& io_context, SocketId socket)
{
return io_context.lowLevelProvider->wrapSocketFd(socket, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
}

//! Event loop implementation.
//!
//! Cap'n Proto threading model is very simple: all I/O operations are
Expand Down Expand Up @@ -308,11 +324,12 @@ class EventLoop
//! Callback functions to run on async thread.
std::optional<CleanupList> m_async_fns MP_GUARDED_BY(m_mutex);

//! Pipe read handle used to wake up the event loop thread.
int m_wait_fd = -1;
//! Socket pair used to post and wait for wakeups to the event loop thread.
kj::Own<kj::AsyncIoStream> m_wait_stream;
kj::Own<kj::AsyncIoStream> m_post_stream;

//! Pipe write handle used to wake up the event loop thread.
int m_post_fd = -1;
//! Synchronous writer used to write to m_post_stream.
kj::Own<kj::OutputStream> m_post_writer;

//! Number of clients holding references to ProxyServerBase objects that
//! reference this event loop.
Expand Down Expand Up @@ -793,17 +810,15 @@ kj::Promise<T> ProxyServer<Thread>::post(Fn&& fn)
return ret;
}

//! Given stream file descriptor, make a new ProxyClient object to send requests
//! over the stream. Also create a new Connection object embedded in the
//! client that is freed when the client is closed.
//! Given a stream, make a new ProxyClient object to send requests over it.
//! Also create a new Connection object embedded in the client that is freed
//! when the client is closed.
template <typename InitInterface>
std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, int fd)
std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, kj::Own<kj::AsyncIoStream> stream)
{
typename InitInterface::Client init_client(nullptr);
std::unique_ptr<Connection> connection;
loop.sync([&] {
auto stream =
loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP);
connection = std::make_unique<Connection>(loop, kj::mv(stream));
init_client = connection->m_rpc_system->bootstrap(ServerVatId().vat_id).castAs<InitInterface>();
Connection* connection_ptr = connection.get();
Expand Down Expand Up @@ -851,13 +866,12 @@ void _Listen(EventLoop& loop, kj::Own<kj::ConnectionReceiver>&& listener, InitIm
}));
}

//! Given stream file descriptor and an init object, handle requests on the
//! stream by calling methods on the Init object.
//! Given a stream and an init object, handle requests on the stream by calling
//! methods on the Init object.
template <typename InitInterface, typename InitImpl>
void ServeStream(EventLoop& loop, int fd, InitImpl& init)
void ServeStream(EventLoop& loop, kj::Own<kj::AsyncIoStream> stream, InitImpl& init)
{
_Serve<InitInterface>(
loop, loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP), init);
_Serve<InitInterface>(loop, kj::mv(stream), init);
}

//! Given listening socket file descriptor and an init object, handle incoming
Expand Down
10 changes: 5 additions & 5 deletions include/mp/proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,11 @@ static constexpr int FIELD_BOXED = 16;
template <typename Field, int flags>
struct Accessor : public Field
{
static const bool in = flags & FIELD_IN;
static const bool out = flags & FIELD_OUT;
static const bool optional = flags & FIELD_OPTIONAL;
static const bool requested = flags & FIELD_REQUESTED;
static const bool boxed = flags & FIELD_BOXED;
static constexpr bool in = (flags & FIELD_IN) != 0;
static constexpr bool out = (flags & FIELD_OUT) != 0;
static constexpr bool optional = (flags & FIELD_OPTIONAL) != 0;
static constexpr bool requested = (flags & FIELD_REQUESTED) != 0;
static constexpr bool boxed = (flags & FIELD_BOXED) != 0;
};

//! Wrapper around std::function for passing std::function objects between client and servers.
Expand Down
4 changes: 2 additions & 2 deletions include/mp/type-interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ void CustomBuildField(TypeList<Impl&>,
InvokeContext& invoke_context,
Impl& value,
Output&& output,
typename decltype(output.get())::Calls* enable = nullptr)
typename Decay<decltype(output.get())>::Calls* enable = nullptr)
{
// Disable deleter so proxy server object doesn't attempt to delete the
// wrapped implementation when the proxy client is destroyed or
// disconnected.
using Interface = typename decltype(output.get())::Calls;
using Interface = typename Decay<decltype(output.get())>::Calls;
output.set(CustomMakeProxyServer<Interface, Impl>(invoke_context, std::shared_ptr<Impl>(&value, [](Impl*){})));
}

Expand Down
Loading
Loading