treewide: add first batch of capnp rpc types
this touches both libutil and libstore because with no rpc users it doesn't make that much sense to separate the two. note that all our strings are represented as Data (ie, blobs) because capnp Text must be nul-terminated. while it's technically possible to use Text with strings containing non-terminating NULs it is a bit of a hassle and could lead to rpc users erroneously stopping at the first NUL byte. Change-Id: I4c75e03b79a226ffa8d7cd985e3ac632a0cd7c1c
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
libstore_generated_headers = []
|
||||
libstore_generated_sources = []
|
||||
foreach header : [ 'schema.sql' ]
|
||||
libstore_generated_headers += custom_target(
|
||||
command : [ 'bash', '-c', 'echo \'R"__NIX_STR(\' | cat - @INPUT@ && echo \')__NIX_STR"\'' ],
|
||||
@@ -10,6 +11,40 @@ foreach header : [ 'schema.sql' ]
|
||||
)
|
||||
endforeach
|
||||
|
||||
libstore_rpc = []
|
||||
|
||||
libstore_rpc += custom_target(
|
||||
command : [
|
||||
capnpc_wrapper,
|
||||
'--language=c++',
|
||||
'--src-prefix=@CURRENT_SOURCE_DIR@',
|
||||
'--outdir=@OUTDIR@',
|
||||
'--depfile=@DEPFILE@',
|
||||
'-I@SOURCE_ROOT@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : files(
|
||||
# keep-sorted start
|
||||
'types.capnp',
|
||||
# keep-sorted end
|
||||
),
|
||||
output : [
|
||||
'@PLAINNAME@.h',
|
||||
'@PLAINNAME@.c++',
|
||||
],
|
||||
depfile : '@PLAINNAME@.d',
|
||||
)
|
||||
|
||||
foreach i : libstore_rpc
|
||||
foreach rpc_src : i.to_list()
|
||||
if rpc_src.full_path().endswith('.h')
|
||||
libstore_generated_headers += [ rpc_src ]
|
||||
else
|
||||
libstore_generated_sources += [ rpc_src ]
|
||||
endif
|
||||
endforeach
|
||||
endforeach
|
||||
|
||||
libstore_settings_headers = []
|
||||
|
||||
file_transfer_setting_definitions = files(
|
||||
@@ -268,6 +303,7 @@ libstore_headers = files(
|
||||
'store-api.hh',
|
||||
'store-cast.hh',
|
||||
'temporary-dir.hh',
|
||||
'types-rpc.hh',
|
||||
'uds-remote-store.hh',
|
||||
'worker-protocol-impl.hh',
|
||||
'worker-protocol.hh',
|
||||
@@ -356,6 +392,7 @@ dependencies = [
|
||||
aws_sdk_transfer,
|
||||
nlohmann_json,
|
||||
kj,
|
||||
capnp_rpc,
|
||||
]
|
||||
|
||||
if host_machine.system() == 'freebsd'
|
||||
@@ -365,6 +402,7 @@ endif
|
||||
libstore = library(
|
||||
'lixstore',
|
||||
libstore_sources,
|
||||
libstore_generated_sources,
|
||||
libstore_settings_headers,
|
||||
libstore_generated_headers,
|
||||
libstore_extra_objects,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
///@file RPC helper functions for `types.hh`
|
||||
|
||||
#include "lix/libstore/types.capnp.h"
|
||||
#include "lix/libutil/rpc.hh"
|
||||
#include "path.hh"
|
||||
#include "store-api.hh"
|
||||
#include <string_view>
|
||||
|
||||
namespace nix::rpc {
|
||||
inline nix::StorePath from(const StorePath::Reader & sp, auto &&... args)
|
||||
{
|
||||
Store & store = std::get<Store &>(std::tie(args...));
|
||||
return store.parseStorePath(to<std::string_view>(sp.getRaw()));
|
||||
}
|
||||
|
||||
template<>
|
||||
struct Fill<StorePath, nix::StorePath>
|
||||
{
|
||||
static void fill(StorePath::Builder spb, const nix::StorePath & sp, auto &&... args)
|
||||
{
|
||||
Store & store = std::get<Store &>(std::tie(args...));
|
||||
LIX_RPC_FILL(spb, setRaw, store.printStorePath(sp));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@0x8f8131bf93ce599c;
|
||||
|
||||
using Cxx = import "/capnp/c++.capnp";
|
||||
$Cxx.namespace("nix::rpc");
|
||||
|
||||
struct StorePath {
|
||||
raw @0 :Data;
|
||||
}
|
||||
@@ -116,6 +116,8 @@ libutil_headers = files(
|
||||
'regex.hh',
|
||||
'repair-flag.hh',
|
||||
'result.hh',
|
||||
'rpc-fwd.hh',
|
||||
'rpc.hh',
|
||||
'serialise.hh',
|
||||
'shlex.hh',
|
||||
'signals.hh',
|
||||
@@ -130,6 +132,7 @@ libutil_headers = files(
|
||||
'thread-pool.hh',
|
||||
'topo-sort.hh',
|
||||
'tracepoint.hh',
|
||||
'types-rpc.hh',
|
||||
'types.hh',
|
||||
'unix-domain-socket.hh',
|
||||
'url-name.hh',
|
||||
@@ -287,9 +290,44 @@ libutil_settings_headers += custom_target(
|
||||
install_dir : includedir / 'lix/libutil',
|
||||
)
|
||||
|
||||
libutil_rpc_headers = []
|
||||
libutil_rpc_sources = []
|
||||
libutil_rpc = []
|
||||
|
||||
libutil_rpc += custom_target(
|
||||
command : [
|
||||
capnpc_wrapper,
|
||||
'--language=c++',
|
||||
'--src-prefix=@CURRENT_SOURCE_DIR@',
|
||||
'--outdir=@OUTDIR@',
|
||||
'--depfile=@DEPFILE@',
|
||||
'-I@SOURCE_ROOT@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : files(
|
||||
# keep-sorted start
|
||||
'types.capnp',
|
||||
# keep-sorted end
|
||||
),
|
||||
output : [
|
||||
'@PLAINNAME@.h',
|
||||
'@PLAINNAME@.c++',
|
||||
],
|
||||
install : true,
|
||||
install_dir : [includedir / 'lix/libutil', false],
|
||||
depfile : '@PLAINNAME@.d',
|
||||
)
|
||||
|
||||
foreach rpc : libutil_rpc
|
||||
libutil_rpc_headers += rpc[0]
|
||||
libutil_rpc_sources += rpc[1]
|
||||
endforeach
|
||||
|
||||
libutil = library(
|
||||
'lixutil',
|
||||
libutil_sources,
|
||||
libutil_rpc_sources,
|
||||
libutil_rpc_headers,
|
||||
experimental_features_header,
|
||||
experimental_features_impl_header,
|
||||
deprecated_features_header,
|
||||
@@ -307,6 +345,7 @@ libutil = library(
|
||||
openssl,
|
||||
nlohmann_json,
|
||||
kj,
|
||||
capnp_rpc,
|
||||
],
|
||||
include_directories : [ '../..' ],
|
||||
cpp_pch : cpp_pch,
|
||||
@@ -337,6 +376,7 @@ liblixutil = declare_dependency(
|
||||
experimental_features_header,
|
||||
deprecated_features_header,
|
||||
libutil_settings_headers,
|
||||
libutil_rpc_headers,
|
||||
],
|
||||
dependencies: [
|
||||
boost,
|
||||
@@ -344,6 +384,7 @@ liblixutil = declare_dependency(
|
||||
# lix-base pkg-config externally)
|
||||
kj,
|
||||
libarchive,
|
||||
capnp_rpc,
|
||||
],
|
||||
link_with : libutil
|
||||
)
|
||||
@@ -356,6 +397,7 @@ if is_static
|
||||
experimental_features_header,
|
||||
deprecated_features_header,
|
||||
libutil_settings_headers,
|
||||
libutil_rpc_headers,
|
||||
],
|
||||
dependencies: [
|
||||
boost,
|
||||
@@ -363,6 +405,7 @@ if is_static
|
||||
# lix-base pkg-config externally)
|
||||
kj,
|
||||
libarchive,
|
||||
capnp_rpc,
|
||||
],
|
||||
link_whole : libutil,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
///@file RPC helper forward declarations
|
||||
|
||||
namespace nix::rpc {
|
||||
template<typename From, typename To>
|
||||
struct Convert;
|
||||
|
||||
template<typename To, typename From>
|
||||
struct Fill;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#pragma once
|
||||
///@file RPC helper functions
|
||||
|
||||
#include "lix/libutil/charptr-cast.hh"
|
||||
#include "lix/libutil/rpc-fwd.hh"
|
||||
#include <capnp/blob.h>
|
||||
#include <capnp/common.h>
|
||||
#include <capnp/list.h>
|
||||
#include <concepts>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace nix::rpc {
|
||||
|
||||
template<typename To, typename From>
|
||||
requires requires(From f) { Convert<typename From::Reads, To>{}; }
|
||||
To to(const From & from, auto &&... args)
|
||||
{
|
||||
return Convert<typename From::Reads, To>::convert(from, args...);
|
||||
}
|
||||
|
||||
// conversions for capnp Text
|
||||
template<>
|
||||
struct Convert<capnp::Text, std::string_view>
|
||||
{
|
||||
static std::string_view convert(const capnp::Text::Reader & t, auto &&...)
|
||||
{
|
||||
return std::string_view(t.begin(), t.size());
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Convert<capnp::Text, std::string>
|
||||
{
|
||||
static std::string convert(const capnp::Text::Reader & t, auto &&...)
|
||||
{
|
||||
return std::string(to<std::string_view>(t));
|
||||
}
|
||||
};
|
||||
|
||||
// conversions for capnp Data
|
||||
template<>
|
||||
struct Convert<capnp::Data, std::string_view>
|
||||
{
|
||||
static std::string_view convert(const capnp::Data::Reader & t, auto &&...)
|
||||
{
|
||||
return std::string_view(t.asChars().begin(), t.size());
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Convert<capnp::Data, std::string>
|
||||
{
|
||||
static std::string convert(const capnp::Data::Reader & t, auto &&...)
|
||||
{
|
||||
return std::string(to<std::string_view>(t));
|
||||
}
|
||||
};
|
||||
|
||||
// conversions for collections. order is maintained during conversion and turns
|
||||
// into insertion order for the result, i.e. sets and maps will only retain the
|
||||
// first element in the list that compares equivalent to some other list entry.
|
||||
template<typename Elem, typename To>
|
||||
requires requires(To t, capnp::List<Elem>::Reader list) {
|
||||
typename To::value_type;
|
||||
t.insert(t.end(), to<typename To::value_type>(list[0]));
|
||||
}
|
||||
struct Convert<capnp::List<Elem>, To>
|
||||
{
|
||||
static To convert(const capnp::List<Elem>::Reader & list, auto &&... args)
|
||||
{
|
||||
To result;
|
||||
for (auto && t : list) {
|
||||
result.insert(result.end(), to<typename To::value_type>(t, args...));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename From>
|
||||
requires std::convertible_to<std::ranges::range_value_t<From>, std::string>
|
||||
struct Fill<capnp::List<capnp::Text>, From>
|
||||
{
|
||||
static void fill(capnp::List<capnp::Text>::Builder builder, const From & from, auto &&... args)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (const std::string & e : from) {
|
||||
builder.set(i++, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename From>
|
||||
requires std::convertible_to<std::ranges::range_value_t<From>, std::string_view>
|
||||
struct Fill<capnp::List<capnp::Data>, From>
|
||||
{
|
||||
static void fill(capnp::List<capnp::Data>::Builder builder, const From & from, auto &&... args)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (auto && e : from) {
|
||||
std::string_view sv = e;
|
||||
builder.set(i++, {charptr_cast<const unsigned char *>(sv.begin()), sv.size()});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Elem, typename From>
|
||||
requires requires { Fill<Elem, std::ranges::range_value_t<From>>{}; }
|
||||
struct Fill<capnp::List<Elem>, From>
|
||||
{
|
||||
static void fill(capnp::List<Elem>::Builder builder, const From & from, auto &&... args)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (auto && e : from) {
|
||||
Fill<Elem, std::ranges::range_value_t<From>>::fill(builder[i++], e, args...);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// blanket converter that turns a `from` into a `to`
|
||||
template<typename Rpc, typename To>
|
||||
requires requires(typename Rpc::Reader r) {
|
||||
{
|
||||
from(r)
|
||||
} -> std::same_as<To>;
|
||||
}
|
||||
struct Convert<Rpc, To>
|
||||
{
|
||||
static To convert(const typename Rpc::Reader & r, auto &&... args)
|
||||
{
|
||||
return from(r, args...);
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
// strings to Data and Text
|
||||
template<typename Builder, std::convertible_to<std::string_view> From>
|
||||
inline void
|
||||
doFill(auto && builder, void (Builder::*Field)(::capnp::Data::Reader), From && f, auto &&...)
|
||||
{
|
||||
std::string_view sv = f;
|
||||
(builder.*Field)({charptr_cast<const unsigned char *>(sv.begin()), sv.size()});
|
||||
}
|
||||
|
||||
template<typename Builder, std::convertible_to<std::string> From>
|
||||
inline void
|
||||
doFill(auto && builder, void (Builder::*field)(::capnp::Text::Reader), From && f, auto &&...)
|
||||
{
|
||||
(builder.*field)(f);
|
||||
}
|
||||
|
||||
// structs
|
||||
template<typename Builder, typename From, typename Inner>
|
||||
inline void doFill(auto && builder, Inner (Builder::*field)(), From && f, auto &&... args)
|
||||
{
|
||||
Fill<typename Inner::Builds, std::remove_cvref_t<From>>::fill(
|
||||
(builder.*field)(), std::forward<From>(f), args...
|
||||
);
|
||||
}
|
||||
|
||||
// lists and other primitives. lists must be distinguished by having builders.
|
||||
template<typename Builder, typename From, typename Inner, typename Init>
|
||||
inline void doFill(auto && builder, Inner (Builder::*field)(Init), From && f, auto &&... args)
|
||||
{
|
||||
if constexpr (requires {
|
||||
std::same_as<Init, capnp::uint>;
|
||||
typename Inner::Builds;
|
||||
})
|
||||
{
|
||||
Fill<typename Inner::Builds, std::remove_cvref_t<From>>::fill(
|
||||
(builder.*field)(f.size()), std::forward<From>(f), args...
|
||||
);
|
||||
} else {
|
||||
(builder.*field)(std::forward<From>(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define LIX_RPC_FILL(fobj, ffield, fsource, ...) \
|
||||
[&] { ::nix::rpc::detail::doFill(fobj, &decltype(fobj)::ffield, (fsource), ##__VA_ARGS__); }()
|
||||
|
||||
#define LIX_TRY_AWAIT_RPC(...) \
|
||||
LIX_TRY_AWAIT_CONTEXT_MAP( \
|
||||
[] { return "RPC call"; }, \
|
||||
([](auto r) { return ::nix::rpc::from(r.getResult()); }), \
|
||||
__VA_ARGS__ \
|
||||
)
|
||||
|
||||
#ifdef LIX_UR_COMPILER_UWU
|
||||
#define RPC_FILL LIX_RPC_FILL
|
||||
#define TRY_AWAIT_RPC LIX_TRY_AWAIT_RPC
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
///@file RPC helper functions for `types.hh`
|
||||
|
||||
#include "error.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/types.capnp.h"
|
||||
#include "rpc.hh"
|
||||
#include <capnp/any.h>
|
||||
#include <capnp/common.h>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <ranges>
|
||||
#include <type_traits>
|
||||
|
||||
namespace nix::rpc {
|
||||
|
||||
// ensure that verbosity levels match to make conversions trivial below.
|
||||
static_assert(int(Verbosity::ERROR) == int(nix::Verbosity::lvlError));
|
||||
static_assert(int(Verbosity::WARN) == int(nix::Verbosity::lvlWarn));
|
||||
static_assert(int(Verbosity::NOTICE) == int(nix::Verbosity::lvlNotice));
|
||||
static_assert(int(Verbosity::INFO) == int(nix::Verbosity::lvlInfo));
|
||||
static_assert(int(Verbosity::TALKATIVE) == int(nix::Verbosity::lvlTalkative));
|
||||
static_assert(int(Verbosity::CHATTY) == int(nix::Verbosity::lvlChatty));
|
||||
static_assert(int(Verbosity::DEBUG) == int(nix::Verbosity::lvlDebug));
|
||||
static_assert(int(Verbosity::VOMIT) == int(nix::Verbosity::lvlVomit));
|
||||
|
||||
inline nix::ErrorInfo from(const Error::Reader & e, auto &&... args)
|
||||
{
|
||||
ErrorInfo ei{
|
||||
// capnp enums are u16, and we have checked that the enumerator values match
|
||||
.level = nix::Verbosity(std::clamp<std::underlying_type_t<nix::Verbosity>>(
|
||||
uint16_t(e.getLevel()), lvlError, lvlVomit
|
||||
)),
|
||||
.msg = HintFmt(to<std::string>(e.getMessage())),
|
||||
};
|
||||
for (auto && t : e.getTraces()) {
|
||||
ei.traces.push_back(Trace{.hint = HintFmt(to<std::string>(t, args...))});
|
||||
}
|
||||
return ei;
|
||||
}
|
||||
|
||||
template<>
|
||||
struct Fill<Error, nix::ErrorInfo>
|
||||
{
|
||||
static void fill(Error::Builder eb, const nix::ErrorInfo & e, auto &&...)
|
||||
{
|
||||
eb.setLevel(Verbosity(e.level));
|
||||
LIX_RPC_FILL(eb, setMessage, e.msg.str());
|
||||
LIX_RPC_FILL(eb, initTraces, e.traces | std::ranges::views::transform([](auto & trace) {
|
||||
return trace.hint.str();
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
inline void makeBadResult(auto rb, const std::exception_ptr & e)
|
||||
{
|
||||
try {
|
||||
std::rethrow_exception(e);
|
||||
} catch (nix::Error & e) {
|
||||
LIX_RPC_FILL(rb, initBad, e.info());
|
||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||
LIX_RPC_FILL(rb, initBad, nix::Error("caught non-lix exception: %s", e.what()).info());
|
||||
} catch (...) {
|
||||
LIX_RPC_FILL(rb, initBad, nix::Error("caught non-exception! spooky").info());
|
||||
}
|
||||
}
|
||||
|
||||
template<typename>
|
||||
inline constexpr bool IsResult = false;
|
||||
template<typename T>
|
||||
inline constexpr bool IsResult<Result<T>> = true;
|
||||
|
||||
template<typename T>
|
||||
concept ResultReader = IsResult<typename T::Reads>;
|
||||
|
||||
template<typename T>
|
||||
concept ResultBuilder = IsResult<typename T::Builds>;
|
||||
}
|
||||
|
||||
inline nix::Result<void> from(const ResultV::Reader & r, auto &&... args)
|
||||
{
|
||||
if (r.isGood()) {
|
||||
return result::success();
|
||||
} else {
|
||||
return result::failure(nix::Error(from(r.getBad(), args...)));
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
struct Fill<ResultV, nix::Result<void>>
|
||||
{
|
||||
static void fill(ResultV::Builder rb, const nix::Result<void> & r, auto &&...)
|
||||
{
|
||||
if (r.has_value()) {
|
||||
rb.setGood();
|
||||
} else {
|
||||
detail::makeBadResult(rb, r.error());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Fill<ResultV, std::exception_ptr>
|
||||
{
|
||||
static void fill(ResultV::Builder rb, const std::exception_ptr & e, auto &&...)
|
||||
{
|
||||
detail::makeBadResult(rb, e);
|
||||
}
|
||||
};
|
||||
|
||||
inline auto from(detail::ResultReader auto r, auto &&... args)
|
||||
{
|
||||
using R = nix::Result<decltype(r.getGood())>;
|
||||
if (r.isGood()) {
|
||||
return R(result::success(r.getGood()));
|
||||
} else {
|
||||
return R(result::failure(nix::Error(from(r.getBad(), args...))));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
struct Fill<Result<T>, std::exception_ptr>
|
||||
{
|
||||
static void fill(Result<T>::Builder rb, const std::exception_ptr & e, auto &&...)
|
||||
{
|
||||
detail::makeBadResult(rb, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@0xd43cca581e0ebf82;
|
||||
|
||||
using Cxx = import "/capnp/c++.capnp";
|
||||
$Cxx.namespace("nix::rpc");
|
||||
|
||||
enum Verbosity {
|
||||
error @0;
|
||||
warn @1;
|
||||
notice @2;
|
||||
info @3;
|
||||
talkative @4;
|
||||
chatty @5;
|
||||
debug @6;
|
||||
vomit @7;
|
||||
}
|
||||
|
||||
struct Error {
|
||||
level @0 :Verbosity;
|
||||
message @1 :Data;
|
||||
traces @2 :List(Data);
|
||||
}
|
||||
|
||||
struct Result(T) {
|
||||
union {
|
||||
good @0 :T;
|
||||
bad @1 :Error;
|
||||
}
|
||||
}
|
||||
|
||||
# primitives can't be args to generics, so we need to specialize for primitive here.
|
||||
struct ResultV {
|
||||
union {
|
||||
good @0 :Void;
|
||||
bad @1 :Error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user