Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions ci/configs/windows.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CI_DESC="CI job cross-compiling to Windows with MinGW, tested with Wine"
CI_DIR=build-windows
CI_CACHE_NIX_STORE=true
NIX_ARGS=(
--arg windows true
--arg minimal true
)
CMAKE_ARGS=(
-G Ninja
-DCMAKE_SYSTEM_NAME=Windows
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
-DCMAKE_CROSSCOMPILING_EMULATOR=wine
# -Wa,-mbig-obj: template-heavy C++ generates >32K COFF sections per .obj;
# BigCOFF format raises the limit. Must be passed to the assembler via -Wa.
"-DCMAKE_CXX_FLAGS=-Wa,-mbig-obj"
)
# CXX is set by the nix cross shell to the mingw g++ wrapper; cmake picks
# it up from the environment, so no CMAKE_CXX_COMPILER override needed.
BUILD_ARGS=(-k 0)
# Build native mpgen first (as a code generator for cross-compiled test/example targets).
MPGEN_PRE_BUILD=1
52 changes: 52 additions & 0 deletions ci/scripts/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,49 @@ cmake_ver=$(cmake --version | awk '/version/{print $3; exit}')
ver_ge() { [ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]; }

src_dir=$PWD

# If cross-compiling, build native mpgen first so it can be used as a code
# generator when cmake invokes it from add_custom_command (which does not go
# through CMAKE_CROSSCOMPILING_EMULATOR, unlike add_test executables).
if [ -n "${MPGEN_PRE_BUILD-}" ]; then
native_dir="${src_dir}/${CI_DIR}-native"
[ -n "${CI_CLEAN-}" ] && rm -rf "$native_dir"
mkdir -p "$native_dir"
# Unset cross-compilation env vars so cmake uses the native compiler and
# does a native (not cross) build. Key vars to clear:
# CXX/CC/AR/RANLIB/LD - set to cross-compiler by the cross shell
# cmakeFlags - nix cross shell injects -DCMAKE_SYSTEM_NAME=Windows etc.
# Pass NATIVE_CAPNPROTO_PREFIX as CMAKE_PREFIX_PATH so cmake finds the
# native Cap'n Proto rather than the cross-compiled one.
native_cmake_args=()
if [ -n "${NATIVE_CAPNPROTO_PREFIX-}" ]; then
# Build a cmake prefix path with native capnproto and its dependencies
# (openssl, zlib) so find_package and find_dependency succeed.
native_prefix="${NATIVE_CAPNPROTO_PREFIX}"
[ -n "${NATIVE_OPENSSL_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_DEV}"
[ -n "${NATIVE_OPENSSL_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_LIB}"
[ -n "${NATIVE_ZLIB_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_DEV}"
[ -n "${NATIVE_ZLIB_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_LIB}"
native_cmake_args+=(
"-DCMAKE_PREFIX_PATH=${native_prefix}"
"-DCapnProto_DIR=${NATIVE_CAPNPROTO_PREFIX}/lib/cmake/CapnProto"
)
fi
# -static-libstdc++ / -static-libgcc: the native mpgen must run inside the
# cross nix shell where the native libstdc++.so may not be in LD_LIBRARY_PATH.
native_cmake_args+=("-DCMAKE_EXE_LINKER_FLAGS=-static-libstdc++ -static-libgcc")
(cd "$native_dir" && env -u CXX -u CC -u AR -u RANLIB -u LD -u cmakeFlags cmake "$src_dir" "${native_cmake_args[@]+${native_cmake_args[@]}}" && cmake --build . -t mpgen)
CMAKE_ARGS+=("-DMPGEN_EXECUTABLE=${native_dir}/mpgen")

# Override capnp tool executables: the cross capnproto cmake config sets
# CAPNP_EXECUTABLE to capnp.exe (Windows binary), which can't run on Linux.
# Use the native capnp/capnpc-c++ binaries from pkgs.capnproto in nativeBuildInputs.
_capnp=$(command -v capnp 2>/dev/null || true)
_capnpc=$(command -v capnpc-c++ 2>/dev/null || true)
[ -n "$_capnp" ] && CMAKE_ARGS+=("-DCAPNP_EXECUTABLE=$_capnp")
[ -n "$_capnpc" ] && CMAKE_ARGS+=("-DCAPNPC_CXX_EXECUTABLE=$_capnpc")
fi

mkdir -p "$CI_DIR"
cd "$CI_DIR"
cmake "$src_dir" "${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}"
Expand All @@ -34,4 +77,13 @@ else
cmake --build . --target "$t" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
done
fi
# When cross-compiling for Windows, copy GCC and MCF thread runtime DLLs
# alongside the test executables so wine can find them (wine DLL loading
# checks the executable's directory first, before any search-path logic).
if [ -n "${WIN_RUNTIME_DLLS-}" ]; then
IFS=: read -ra _dll_dirs <<< "$WIN_RUNTIME_DLLS"
for _dir in "${_dll_dirs[@]}"; do
find "$_dir" -maxdepth 1 -name "*.dll" -exec cp -n {} test/ \;
done
fi
ctest --output-on-failure
4 changes: 2 additions & 2 deletions example/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ namespace fs = std::filesystem;

static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
{
const auto [pid, socket] = mp::SpawnProcess([&](mp::SpawnConnectInfo info) -> std::vector<std::string> {
const auto [pid, socket] = mp::SpawnProcess([&](std::string connect_info) -> std::vector<std::string> {
fs::path path = process_argv0;
path.remove_filename();
path.append(new_exe_name);
return {path.string(), std::move(info)};
return {path.string(), std::move(connect_info)};
});
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, mp::MakeStream(loop, socket)), pid);
}
Expand Down
50 changes: 35 additions & 15 deletions include/mp/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
#include <memory>
#endif

#ifdef WIN32
// WIN32_LEAN_AND_MEAN excludes commdlg.h which defines `#define INTERFACE
// IPrintDialogServices` — this conflicts with capnp::Kind::INTERFACE used
// in CAPNP_DECLARE_INTERFACE_HEADER. Must be defined before winsock2.h.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <winsock2.h>
#endif

namespace mp {

//! Generic utility functions used by capnp code.
Expand Down Expand Up @@ -273,26 +284,35 @@ std::string LogEscape(const kj::StringTree& string, size_t max_size);

using Stream = kj::Own<kj::AsyncIoStream>;

#ifdef WIN32
// On Windows, ProcessId is defined to be the local process HANDLE rather than
// global process ID, because handles are more useful for controlling and
// waiting for processes. It it possible to obtain the actual process ID from
// handles by calling the GetProcessId API.
using ProcessId = HANDLE;
using SocketId = SOCKET;
constexpr SocketId SocketError{INVALID_SOCKET};
#else
using ProcessId = int;
using SocketId = int;
constexpr SocketId SocketError{-1};

//! Information about parent process passed to child process as a command-line
//! argument. On unix this is the child socket fd number formatted as a string.
using SpawnConnectInfo = std::string;

//! Callback type used by SpawnProcess below.
using SpawnConnectInfoToArgsFn = std::function<std::vector<std::string>(const SpawnConnectInfo&)>;
#endif

//! Spawn a new process that communicates with the current process over a socket
//! pair. Calls connect_info_to_args callback with a connection string that
//! needs to be passed to the child process, and executes the argv command line
//! it returns. Returns child process id and socket id.
std::tuple<ProcessId, SocketId> SpawnProcess(SpawnConnectInfoToArgsFn&& connect_info_to_args);

//! Initialize spawned child process using the SpawnConnectInfo string passed to it,
//! returning a socket id for communicating with the parent process.
SocketId StartSpawned(const SpawnConnectInfo& connect_info);
//! pair. Calls spawn_argv callback with a connection string that needs to be
//! passed to the child process, and executes the argv command line it returns.
//! Returns child process id and socket id.
//!
//! The connection string is just a file descriptor number on unix. On windows,
//! it is a path to a named pipe the parent process will write
//! WSADuplicateSocket info to. In both cases, the child process can call
//! SpawnProcess to get a socket handle from the connection string.
std::tuple<ProcessId, SocketId> SpawnProcess(const std::function<std::vector<std::string>(std::string)>& spawn_argv);

//! Initialize spawned child process using the connection string passed by
//! SpawnProcess through its command line arguments. Returns socket id for
//! communicating with the parent process.
SocketId StartSpawned(const std::string& connect_info);

//! Create a socket pair that can be used to communicate within a process or
//! between parent and child processes.
Expand Down
77 changes: 71 additions & 6 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{ pkgs ? import <nixpkgs> {}
, crossPkgs ? import <nixpkgs> {}
, crossPkgs ? null # null means same as pkgs; overrides windows when set explicitly
, windows ? false # Cross-compile for Windows using MinGW; implies crossPkgs = pkgs.pkgsCross.mingwW64
, enableLibcxx ? false # Whether to use libc++ toolchain and libraries instead of libstdc++
, minimal ? false # Whether to create minimal shell without extra tools (faster when cross compiling)
, capnprotoVersion ? null
Expand All @@ -10,7 +11,11 @@

let
lib = pkgs.lib;
llvmBase = crossPkgs.llvmPackages_21;
effectiveCrossPkgs =
if crossPkgs != null then crossPkgs
else if windows then pkgs.pkgsCross.mingwW64
else pkgs;
llvmBase = pkgs.llvmPackages_21;
llvm = llvmBase // lib.optionalAttrs (libcxxSanitizers != null) {
libcxx = llvmBase.libcxx.override {
devExtraCmakeFlags = [ "-DLLVM_USE_SANITIZER=${libcxxSanitizers}" ];
Expand All @@ -30,9 +35,9 @@ let
"1.1.0" = "sha256-gxkko7LFyJNlxpTS+CWOd/p9x/778/kNIXfpDGiKM2A=";
"1.2.0" = "sha256-aDcn4bLZGq8915/NPPQsN5Jv8FRWd8cAspkG3078psc=";
};
capnprotoBase = if capnprotoVersion == null then crossPkgs.capnproto else crossPkgs.capnproto.overrideAttrs (old: {
capnprotoBase = if capnprotoVersion == null then effectiveCrossPkgs.capnproto else effectiveCrossPkgs.capnproto.overrideAttrs (old: {
version = capnprotoVersion;
src = crossPkgs.fetchFromGitHub {
src = effectiveCrossPkgs.fetchFromGitHub {
owner = "capnproto";
repo = "capnproto";
rev = "v${capnprotoVersion}";
Expand All @@ -53,7 +58,41 @@ let
"-g"
];
};
})).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; });
} // lib.optionalAttrs windows {
# Cross-compiling capnproto for Windows with the nixpkgs llvm-mingw toolchain
# requires several cmake/nix workarounds:
#
# - BUILD_SHARED_LIBS=FALSE: static libs mean mptest.exe is self-contained,
# simplifying wine execution (no DLL search path needed).
# - WITH_FIBERS=FALSE: kj fiber support on Windows/MinGW may not build.
#
# Two nix environment issues also need fixing:
# 1. The clang wrapper uses GCC's C++ headers (libstdc++), which on this
# GCC version use the MCF thread model. MCF headers are in a separate
# package (windows.mcfgthreads.dev) not in capnproto's default buildInputs.
# 2. The clang wrapper's cc-ldflags is missing the GCC target lib directory
# that contains libgcc_s.a. We add it in preConfigure.
buildInputs = (old.buildInputs or []) ++ [
effectiveCrossPkgs.windows.mcfgthreads.dev # provides mcfgthread/gthr.h
effectiveCrossPkgs.windows.mcfgthreads # provides libmcfgthread.a for linking
];
preConfigure = (old.preConfigure or "") + ''
# The nixpkgs clang-mingw wrapper omits the GCC target lib directory
# ($gcc/x86_64-w64-mingw32/lib) from its search path, so the linker
# can't find libgcc_s.a even though the file exists. Add it explicitly.
# Must use 'export' so child processes (cmake, linker) inherit the value.
export NIX_LDFLAGS_x86_64_w64_mingw32="''${NIX_LDFLAGS_x86_64_w64_mingw32:-} -L${effectiveCrossPkgs.buildPackages.gcc.cc}/x86_64-w64-mingw32/lib"
'';
cmakeFlags = (old.cmakeFlags or []) ++ [
"-DBUILD_SHARED_LIBS=FALSE"
"-DWITH_FIBERS=FALSE"
];
})).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; }
# Switch capnproto's cross-build to effectiveCrossPkgs.stdenv (GCC) instead of
# the default clangStdenv. Clang with GCC's libstdc++.a causes MCF thread
# symbol errors (_MCF_mutex_lock_slow etc.) because clang doesn't automatically
# link GCC's MCF runtime. GCC knows its own runtime and links it correctly.
// lib.optionalAttrs windows { clangStdenv = effectiveCrossPkgs.stdenv; });
clang = if enableLibcxx then llvm.libcxxClang else llvm.clang;
clang-tools = llvm.clang-tools.override { inherit enableLibcxx; };
cmakeHashes = {
Expand All @@ -67,7 +106,7 @@ let
};
patches = [];
})).override { isMinimalBuild = true; };
in crossPkgs.mkShell {
in effectiveCrossPkgs.mkShell {
buildInputs = [
capnproto
];
Expand All @@ -78,8 +117,34 @@ in crossPkgs.mkShell {
] ++ lib.optionals (!minimal) [
clang
clang-tools
] ++ lib.optionals windows [
pkgs.capnproto # native capnp + capnpc-c++ in PATH for cmake code generation
pkgs.wine64Packages.staging # run cross-compiled mptest.exe in ctest
pkgs.gcc # native C++ compiler for the native mpgen pre-build
pkgs.openssl.dev # native capnproto cmake config: find_dependency(OpenSSL) headers
pkgs.openssl.out # native capnproto cmake config: find_dependency(OpenSSL) libs
pkgs.zlib.dev # native capnproto cmake config: find_dependency(ZLIB) headers
pkgs.zlib # native capnproto cmake config: find_dependency(ZLIB) libs
];

# Tell IWYU where its libc++ mapping lives
IWYU_MAPPING_FILE = if enableLibcxx then "${llvm.libcxx.dev}/include/c++/v1/libcxx.imp" else null;

# When cross-compiling, expose native package prefixes so ci.sh can point
# the native mpgen pre-build's CMAKE_PREFIX_PATH at them. CMAKE_PREFIX_PATH
# in the cross shell points at the Windows packages, not the native ones.
NATIVE_CAPNPROTO_PREFIX = if windows then "${pkgs.capnproto}" else null;
# OpenSSL and zlib are dependencies of the native capnproto cmake config
# (find_dependency calls); the native cmake build needs to find them too.
# FindOpenSSL.cmake needs both the dev output (headers) and out (libs).
NATIVE_OPENSSL_DEV = if windows then "${pkgs.openssl.dev}" else null;
NATIVE_OPENSSL_LIB = if windows then "${pkgs.openssl.out}" else null;
NATIVE_ZLIB_DEV = if windows then "${pkgs.zlib.dev}" else null;
NATIVE_ZLIB_LIB = if windows then "${pkgs.zlib}" else null;
# GCC and MCF thread runtime DLL directories needed by wine to run mptest.exe.
# libstdc++-6.dll and libgcc_s_seh-1.dll are in the GCC cross-compiler lib dir;
# libmcfgthread-2.dll is in the mcfgthreads package's bin dir.
WIN_RUNTIME_DLLS = if windows then
"${effectiveCrossPkgs.buildPackages.gcc.cc.lib}/x86_64-w64-mingw32/lib:${effectiveCrossPkgs.windows.mcfgthreads}/bin"
else null;
}
38 changes: 38 additions & 0 deletions src/mp/proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ void Connection::removeSyncCleanup(CleanupIt it)
m_sync_cleanup_fns.erase(it);
}

#ifdef WIN32
//! Synchronous socket output stream. Cap'n Proto library only provides limited
//! support for synchronous IO. It provides `FdOutputStream` which wraps unix
//! file descriptors and calls write() internally, and `HandleOutStream` which
//! wraps windows HANDLE values and calls WriteFile() internally. This class
//! just provides analogous functionality wrapping SOCKET values and calls
//! send() internally.
class SocketOutputStream : public kj::OutputStream {
public:
explicit SocketOutputStream(SOCKET socket) : m_socket(socket) {}

void write(const void* buffer, size_t size) override;

private:
SOCKET m_socket;
};

static constexpr size_t WRITE_CLAMP_SIZE = 1u << 30; // 1GB clamp for Windows, like FdOutputStream

void SocketOutputStream::write(const void* buffer, size_t size) {
const char* pos = reinterpret_cast<const char*>(buffer);

while (size > 0) {
int n = send(m_socket, pos, static_cast<int>(kj::min(size, WRITE_CLAMP_SIZE)), 0);

KJ_WIN32(n != SOCKET_ERROR, "send() failed");
KJ_ASSERT(n > 0, "send() returned zero.");

pos += n;
size -= n;
}
}
#endif

void EventLoop::addAsyncCleanup(std::function<void()> fn)
{
const Lock lock(m_mutex);
Expand Down Expand Up @@ -261,6 +295,10 @@ EventLoop::EventLoop(const char* exe_name, LogOptions log_opts, void* context)
m_post_stream = kj::mv(pipe.ends[1]);
KJ_IF_MAYBE(fd, m_post_stream->getFd()) {
m_post_writer = kj::heap<kj::FdOutputStream>(*fd);
#ifdef WIN32
} else KJ_IF_MAYBE(handle, m_post_stream->getWin32Handle()) {
m_post_writer = kj::heap<SocketOutputStream>(reinterpret_cast<SOCKET>(*handle));
#endif
} else {
throw std::logic_error("Could not get file descriptor for new pipe.");
}
Expand Down
Loading
Loading