summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/arti-rpc-client-core/Cargo.toml6
-rw-r--r--crates/arti-rpc-client-core/README.md1
-rw-r--r--crates/arti-rpc-client-core/arti-rpc-client-core.h410
-rw-r--r--crates/arti-rpc-client-core/cbindgen.toml178
-rw-r--r--crates/arti-rpc-client-core/cbindgen.warnings52
-rw-r--r--crates/arti-rpc-client-core/src/conn.rs30
-rw-r--r--crates/arti-rpc-client-core/src/ffi.rs174
-rw-r--r--crates/arti-rpc-client-core/src/ffi/err.rs436
-rw-r--r--crates/arti-rpc-client-core/src/ffi/util.rs592
-rw-r--r--crates/arti-rpc-client-core/src/lib.rs7
-rw-r--r--crates/arti-rpc-client-core/src/msgs/response.rs30
-rw-r--r--crates/arti-rpc-client-core/src/util.rs60
12 files changed, 1948 insertions, 28 deletions
diff --git a/crates/arti-rpc-client-core/Cargo.toml b/crates/arti-rpc-client-core/Cargo.toml
index 9a9da51ac..897d52761 100644
--- a/crates/arti-rpc-client-core/Cargo.toml
+++ b/crates/arti-rpc-client-core/Cargo.toml
@@ -14,12 +14,15 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
[dependencies]
+c_str_macro = { version = "1", optional = true }
caret = { path = "../caret", version = "0.4.5" }
derive_more = "0.99.3"
educe = "0.4.6"
+paste = { version = "1", optional = true }
serde = { version = "1.0.103", features = ["derive"] }
serde_json = "1.0.104"
thiserror = "1"
+void = "1"
[dev-dependencies]
rand = "0.8"
@@ -28,7 +31,8 @@ socketpair = "0.19"
tor-basic-utils = { path = "../tor-basic-utils", version = "0.20.0" }
[features]
-full = []
+full = ["ffi"]
+ffi = ["c_str_macro", "paste"]
[package.metadata.docs.rs]
all-features = true
diff --git a/crates/arti-rpc-client-core/README.md b/crates/arti-rpc-client-core/README.md
index a2fc0773f..5898a8cef 100644
--- a/crates/arti-rpc-client-core/README.md
+++ b/crates/arti-rpc-client-core/README.md
@@ -22,6 +22,7 @@ Notes so far:
* [ ] More tests.
* [ ] update this readme.
* [x] interface for connecting to arti
+ * [x] Initial C FFI wrappers.
* [ ] C FFI wrappers for everything reasonable
* [x] enable the usual warnings.
* [ ] Finish this readme.
diff --git a/crates/arti-rpc-client-core/arti-rpc-client-core.h b/crates/arti-rpc-client-core/arti-rpc-client-core.h
new file mode 100644
index 000000000..d7bb581a4
--- /dev/null
+++ b/crates/arti-rpc-client-core/arti-rpc-client-core.h
@@ -0,0 +1,410 @@
+/**
+ * # Arti RPC core library header.
+ *
+ * (TODO RPC: This is still a work in progress; please don't rely on it
+ * being the final API.)
+ *
+ * ## What this library does
+ *
+ * The Arti RPC system works by establishing connections to an Arti instance,
+ * and then exchanging requests and replies in a format inspired by
+ * JSON-RPC. This library takes care of the work of connecting to an Arti
+ * instance, authenticating, validating outgoing JSON requests, and matching
+ * their corresponding JSON responses as they arrive.
+ *
+ * This library _does not_ do the work of creating well-formed requests,
+ * or interpreting the responses.
+ *
+ * (Note: Despite this library being exposed via a set of C functions,
+ * we don't actually expect you to use it from C. It's probably a better
+ * idea to wrap it in a higher-level language and then use it from there.)
+ *
+ * ## Using this library
+ *
+ * TODO RPC Explain better.
+ *
+ * Your connection to Arti is represented by an `ArtiRpcConn *`. Use
+ * `arti_rpc_connect()` to create one of these.
+ *
+ * Once you have a connection, you can sent Arti various requests in
+ * JSON format. See (TODO RPC: Add a link to a list of comments.)
+ * Use `arti_rpc_execute()` to send a simple request; the function will
+ * return when the request succeeds, or fails.
+ *
+ * TODO: Explain handles and other APIs once I add those APIs.
+ *
+ * Except when noted otherwise, all functions in this library are thread-safe.
+ *
+ * ## Error handling
+ *
+ * On success, fallible functions return `ARTI_RPC_STATUS_SUCCESS`. On failure,
+ * they return some other error code, and set an `* error_out` parameter
+ * to a newly allocated `ArtiRpcError` object.
+ * (If `error_out==NULL`, then no error is allocated.)
+ *
+ * You can access information about the an `ArtiRpcError`
+ * by calling `arti_rpc_err_{status,message,response}()` on it.
+ * When you are done with an error, you should free it with
+ * `arti_rpc_err_free()`.
+ *
+ * The `error_out` parameter always appears last.
+ *
+ * ## Interface conventions
+ *
+ * - All functions check for NULL pointers in their arguments.
+ * - As in C tor, `foo_free()` functions treat `foo_free(NULL)` as a no-op.
+ *
+ * - All input strings should be valid UTF-8. (The library will check.)
+ * All output strings will be valid UTF-8.
+ *
+ * - Fallible functions return an ArtiStatus.
+ *
+ * - All identifiers are prefixed with `ARTI_RPC`, `ArtiRpc`, or `arti_rpc` as appropriate.
+ *
+ * - Newly allocated objects are returned via out-parameters,
+ * with `out` in their names.
+ * (For example, `ArtiRpcObject **out`). In such cases, `* out` will be set to a resulting object,
+ * or to NULL if no such object is returned. Any earlier value of `*out` will be replaced
+ * without freeing it.
+ * (If `out` is NULL, then any object the library would have returned will instead be discareded.)
+ * discarded.
+ * While the function is running,
+ * `*out` and `**out` may not be read or written by any other part of the program,
+ * and they may not alias any other arguments.)
+ * - Note that `*out` will be set to NULL if an error occurs
+ * or the function's inputs are invalid.
+ * (The `*error_out` parameter, of course,
+ * is set to NULL when there is _no_ error, and to an error otherwise.)
+ *
+ * - When any object is exposed as a non-const pointer,
+ * the application becomes the owner of that object.
+ * The application is expected to eventually free that object via the corresponding `arti_rpc_*_free()` function.
+ *
+ * - When any object is exposed via a const pointer,
+ * that object is *not* owned by the application.
+ * That object's lifetime will be as documented.
+ * The application must not modify or free such an object.
+ *
+ * - If a function should be considered a method on a given type of object,
+ * it will take a pointer to that object as its first argument.
+ *
+ * - If a function consumes (takes ownership of) one of its inputs,
+ * it does so regardless of whether the function succeeds or fails.
+ *
+ * ## Correctness requirements
+ *
+ * If any correctness requirements stated here or elsewhere are violated,
+ * it is Undefined Behaviour.
+ * Violations will not be detected by the library.
+ *
+ * - Basic C rules apply:
+ * - If you pass a non-NULL pointer to a function, the pointer must be properly aligned.
+ * It must point to valid, initialized data of the correct type.
+ * - As an exception, functions that take a `Type **out` parameter allow the value of `*out`
+ * (but not `out` itself!) to be uninitialized.
+ * - If you receive data via a `const *`, you must not modify that data.
+ * - If you receive a pointer of type `struct Type *`,
+ * and we do not give you the definition of `struct Type`,
+ * you must not attempt to dereference the pointer.
+ * - You may not call any `_free()` function on an object that is currently in use.
+ * - After you have `_freed()` an object, you may not use it again.
+ * - Every object allocated by this library has a corresponding `*_free()` function:
+ * You must not use libc's free() to free such objects.
+ * - All objects passed as input to a library function must not be mutated
+ * while that function is running.
+ * - All objects passed as input to a library function via a non-const pointer
+ * must not be mutated, inspected, or passed to another library function
+ * while the function is running.
+ * - Furthermore, if a function takes any non-const pointer arguments,
+ * those arguments must not alias one another,
+ * and must not alias any const arguments passed to the function.
+ * - All `const char*` passed as inputs to library functions
+ * are nul-terminated strings.
+ * Additionally, they must be no larger than `SSIZE_MAX`,
+ including the nul.
+ * - If a function takes any mutable pointers
+ **/
+
+#ifndef ARTI_RPC_CLIENT_CORE_H_
+#define ARTI_RPC_CLIENT_CORE_H_
+
+/* Automatically generated by cbindgen. Don't modify manually. */
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/**
+ * A string that is guaranteed to be UTF-8 and NUL-terminated,
+ * for fast access as either type.
+ */
+typedef struct Utf8CString Utf8CString;
+
+/**
+ * A status code returned by an Arti RPC function.
+ *
+ * On success, a function will return `ARTI_SUCCESS (0)`.
+ * On failure, a function will return some other status code.
+ */
+typedef uint32_t ArtiRpcStatus;
+
+/**
+ * An open connection to Arti over an a RPC protocol.
+ *
+ * This is a thread-safe type: you may safely use it from multiple threads at once.
+ *
+ * Once you are no longer going to use this connection at all, you must free
+ * it with [`arti_rpc_conn_free`]
+ */
+typedef struct ArtiRpcConn ArtiRpcConn;
+
+/**
+ * An error returned by the Arti RPC code, exposed as an object.
+ *
+ * When a function returns an [`ArtiRpcStatus`] other than [`ARTI_RPC_STATUS_SUCCESS`],
+ * it will also expose a newly allocated value of this type
+ * via its `error_out` parameter.
+ */
+typedef struct ArtiRpcError ArtiRpcError;
+
+/**
+ * An owned string, returned by this library.
+ *
+ * This string must be released with `arti_rpc_str_free`.
+ * You can inspect it with `arti_rpc_str_get`, but you may not modify it.
+ * The string is guaranteed to be UTF-8 and NUL-terminated.
+ */
+typedef struct Utf8CString ArtiRpcStr;
+
+/**
+ * The function has returned successfully.
+ */
+#define ARTI_RPC_STATUS_SUCCESS 0
+
+/**
+ * One or more of the inputs to a library function was invalid.
+ *
+ * (This error was generated by the library, before any request was sent.)
+ */
+#define ARTI_RPC_STATUS_INVALID_INPUT 1
+
+/**
+ * Tried to use some functionality
+ * (for example, an authentication method or connection scheme)
+ * that wasn't available on this platform or build.
+ *
+ * (This error was generated by the library, before any request was sent.)
+ */
+#define ARTI_RPC_STATUS_NOT_SUPPORTED 2
+
+/**
+ * Tried to connect to Arti, but an IO error occurred.
+ *
+ * This may indicate that Arti wasn't running,
+ * or that Arti was built without RPC support,
+ * or that Arti wasn't running at the specified location.
+ *
+ * (This error was generated by the library.)
+ */
+#define ARTI_RPC_STATUS_CONNECT_IO 3
+
+/**
+ * We tried to authenticate with Arti, but it rejected our attempt.
+ *
+ * (This error was sent by the peer.)
+ */
+#define ARTI_RPC_STATUS_BAD_AUTH 4
+
+/**
+ * Our peer has, in some way, violated the Arti-RPC protocol.
+ *
+ * (This error was generated by the library,
+ * based on a response from Arti that appeared to be invalid.)
+ */
+#define ARTI_RPC_STATUS_PEER_PROTOCOL_VIOLATION 5
+
+/**
+ * The peer has closed our connection; possibly because it is shutting down.
+ *
+ * (This error was generated by the library,
+ * based on the connection being closed or reset from the peer.)
+ */
+#define ARTI_RPC_STATUS_SHUTDOWN 6
+
+/**
+ * An internal error occurred in the arti rpc client.
+ *
+ * (This error was generated by the library.
+ * If you see it, there is probably a bug in the library.)
+ */
+#define ARTI_RPC_STATUS_INTERNAL 7
+
+/**
+ * The peer reports that one of our requests has failed.
+ *
+ * (This error was sent by the peer, in response to one of our requests.
+ * No further responses to that request will be received or accepted.)
+ */
+#define ARTI_RPC_STATUS_REQUEST_FAILED 8
+
+/**
+ * Tried to check the status of a request and found that it was no longer running.
+ *
+ * TODO RPC: We should make sure that this is the actual semantics we want for this
+ * error! Revisit after we have implemented real cancellation.
+ */
+#define ARTI_RPC_STATUS_REQUEST_CANCELLED 9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+/**
+ * Try to open a new connection to an Arti instance.
+ *
+ * The location of the instance and the method to connect to it are described in
+ * `connection_string`.
+ *
+ * (TODO RPC: Document the format of this string better!)
+ *
+ * On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*rpc_conn_out` to a new ArtiRpcConn.
+ * Otherwise return some other status code, set `*rpc_conn_out` to NULL, and set
+ * `*error_out` (if provided) to a newly allocated error object.
+ *
+ *
+ * # Ownership
+ *
+ * The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`,
+ * if set, are eventually freed.
+ */
+ArtiRpcStatus arti_rpc_connect(const char *connection_string,
+ ArtiRpcConn **rpc_conn_out,
+ ArtiRpcError **error_out);
+
+/**
+ * Run an RPC request over `rpc_conn` and wait for a successful response.
+ *
+ * The message `msg` should be a valid RPC request in JSON format.
+ * If you omit its `id` field, one will be generated: this is typically the best way to use this function.
+ *
+ * On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string
+ * containing the JSON response to your request (including `id` and `response` fields).
+ *
+ * Otherwise return some other status code, set `*response_out` to NULL,
+ * and set `*error_out` (if provided) to a newly allocated error object.
+ *
+ * (If response_out is set to NULL, then any successful response will be ignored.)
+ *
+ * # Ownership
+ *
+ * The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
+ */
+ArtiRpcStatus arti_rpc_conn_execute(const ArtiRpcConn *rpc_conn,
+ const char *msg,
+ ArtiRpcStr **response_out,
+ ArtiRpcError **error_out);
+
+/**
+ * Free a string returned by the Arti RPC API.
+ */
+void arti_rpc_str_free(ArtiRpcStr *string);
+
+/**
+ * Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`.
+ *
+ * The resulting string is guaranteed to be valid UTF-8.
+ *
+ * (Returns NULL if the input is NULL.)
+ *
+ * # Correctness requirements
+ *
+ * The resulting string pointer is valid only for as long as the input `string` is not freed.
+ */
+const char *arti_rpc_str_get(const ArtiRpcStr *string);
+
+/**
+ * Close and free an open Arti RPC connection.
+ */
+void arti_rpc_conn_free(ArtiRpcConn *rpc_conn);
+
+/**
+ * Return a string representing the meaning of a given `ArtiRpcStatus`.
+ *
+ * The result will always be non-NULL, even if the status is unrecognized.
+ */
+const char *arti_status_to_str(ArtiRpcStatus status);
+
+/**
+ * Return the status code associated with a given error.
+ *
+ * If `err` is NULL, return [`ARTI_RPC_STATUS_INVALID_INPUT`].
+ */
+ArtiRpcStatus arti_rpc_err_status(const ArtiRpcError *err);
+
+/**
+ * Return a human-readable error message associated with a given error.
+ *
+ * The format of these messages may change arbitrarily between versions of this library;
+ * it is a mistake to depend on the actual contents of this message.
+ *
+ * Return NULL if the input `err` is NULL.
+ *
+ * # Correctness requirements
+ *
+ * The resulting string pointer is valid only for as long as the input `err` is not freed.
+ */
+const char *arti_rpc_err_message(const ArtiRpcError *err);
+
+/**
+ * Return a Json-formatted error response associated with a given error.
+ *
+ * These messages are full responses, including the `error` field,
+ * and the `id` field (if present).
+ *
+ * Return NULL if the specified error does not represent an RPC error response.
+ *
+ * Return NULL if the input `err` is NULL.
+ *
+ * # Correctness requirements
+ *
+ * The resulting string pointer is valid only for as long as the input `err` is not freed.
+ */
+const char *arti_rpc_err_response(const ArtiRpcError *err);
+
+/**
+ * Make and return copy of a provided error.
+ *
+ * Return NULL if the input is NULL.
+ *
+ * # Ownership
+ *
+ * The caller is responsible for making sure that the returned object
+ * is eventually freed with `arti_rpc_err_free()`.
+ */
+ArtiRpcError *arti_rpc_err_clone(const ArtiRpcError *err);
+
+/**
+ * Release storage held by a provided error.
+ */
+void arti_rpc_err_free(ArtiRpcError *err);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
+
+#endif /* ARTI_RPC_CLIENT_CORE_H_ */
diff --git a/crates/arti-rpc-client-core/cbindgen.toml b/crates/arti-rpc-client-core/cbindgen.toml
new file mode 100644
index 000000000..b55db210f
--- /dev/null
+++ b/crates/arti-rpc-client-core/cbindgen.toml
@@ -0,0 +1,178 @@
+
+# We emit a C header by default.
+language = "C"
+
+# We use this macro to prevent double-includes of our header.
+include_guard = "ARTI_RPC_CLIENT_CORE_H_"
+
+# This appears at the top of the file.
+header = """\
+/**
+ * # Arti RPC core library header.
+ *
+ * (TODO RPC: This is still a work in progress; please don't rely on it
+ * being the final API.)
+ *
+ * ## What this library does
+ *
+ * The Arti RPC system works by establishing connections to an Arti instance,
+ * and then exchanging requests and replies in a format inspired by
+ * JSON-RPC. This library takes care of the work of connecting to an Arti
+ * instance, authenticating, validating outgoing JSON requests, and matching
+ * their corresponding JSON responses as they arrive.
+ *
+ * This library _does not_ do the work of creating well-formed requests,
+ * or interpreting the responses.
+ *
+ * (Note: Despite this library being exposed via a set of C functions,
+ * we don't actually expect you to use it from C. It's probably a better
+ * idea to wrap it in a higher-level language and then use it from there.)
+ *
+ * ## Using this library
+ *
+ * TODO RPC Explain better.
+ *
+ * Your connection to Arti is represented by an `ArtiRpcConn *`. Use
+ * `arti_rpc_connect()` to create one of these.
+ *
+ * Once you have a connection, you can sent Arti various requests in
+ * JSON format. See (TODO RPC: Add a link to a list of comments.)
+ * Use `arti_rpc_execute()` to send a simple request; the function will
+ * return when the request succeeds, or fails.
+ *
+ * TODO: Explain handles and other APIs once I add those APIs.
+ *
+ * Except when noted otherwise, all functions in this library are thread-safe.
+ *
+ * ## Error handling
+ *
+ * On success, fallible functions return `ARTI_RPC_STATUS_SUCCESS`. On failure,
+ * they return some other error code, and set an `* error_out` parameter
+ * to a newly allocated `ArtiRpcError` object.
+ * (If `error_out==NULL`, then no error is allocated.)
+ *
+ * You can access information about the an `ArtiRpcError`
+ * by calling `arti_rpc_err_{status,message,response}()` on it.
+ * When you are done with an error, you should free it with
+ * `arti_rpc_err_free()`.
+ *
+ * The `error_out` parameter always appears last.
+ *
+ * ## Interface conventions
+ *
+ * - All functions check for NULL pointers in their arguments.
+ * - As in C tor, `foo_free()` functions treat `foo_free(NULL)` as a no-op.
+ *
+ * - All input strings should be valid UTF-8. (The library will check.)
+ * All output strings will be valid UTF-8.
+ *
+ * - Fallible functions return an ArtiStatus.
+ *
+ * - All identifiers are prefixed with `ARTI_RPC`, `ArtiRpc`, or `arti_rpc` as appropriate.
+ *
+ * - Newly allocated objects are returned via out-parameters,
+ * with `out` in their names.
+ * (For example, `ArtiRpcObject **out`). In such cases, `* out` will be set to a resulting object,
+ * or to NULL if no such object is returned. Any earlier value of `*out` will be replaced
+ * without freeing it.
+ * (If `out` is NULL, then any object the library would have returned will instead be discareded.)
+ * discarded.
+ * While the function is running,
+ * `*out` and `**out` may not be read or written by any other part of the program,
+ * and they may not alias any other arguments.)
+ * - Note that `*out` will be set to NULL if an error occurs
+ * or the function's inputs are invalid.
+ * (The `*error_out` parameter, of course,
+ * is set to NULL when there is _no_ error, and to an error otherwise.)
+ *
+ * - When any object is exposed as a non-const pointer,
+ * the application becomes the owner of that object.
+ * The application is expected to eventually free that object via the corresponding `arti_rpc_*_free()` function.
+ *
+ * - When any object is exposed via a const pointer,
+ * that object is *not* owned by the application.
+ * That object's lifetime will be as documented.
+ * The application must not modify or free such an object.
+ *
+ * - If a function should be considered a method on a given type of object,
+ * it will take a pointer to that object as its first argument.
+ *
+ * - If a function consumes (takes ownership of) one of its inputs,
+ * it does so regardless of whether the function succeeds or fails.
+ *
+ * ## Correctness requirements
+ *
+ * If any correctness requirements stated here or elsewhere are violated,
+ * it is Undefined Behaviour.
+ * Violations will not be detected by the library.
+ *
+ * - Basic C rules apply:
+ * - If you pass a non-NULL pointer to a function, the pointer must be properly aligned.
+ * It must point to valid, initialized data of the correct type.
+ * - As an exception, functions that take a `Type **out` parameter allow the value of `*out`
+ * (but not `out` itself!) to be uninitialized.
+ * - If you receive data via a `const *`, you must not modify that data.
+ * - If you receive a pointer of type `struct Type *`,
+ * and we do not give you the definition of `struct Type`,
+ * you must not attempt to dereference the pointer.
+ * - You may not call any `_free()` function on an object that is currently in use.
+ * - After you have `_freed()` an object, you may not use it again.
+ * - Every object allocated by this library has a corresponding `*_free()` function:
+ * You must not use libc's free() to free such objects.
+ * - All objects passed as input to a library function must not be mutated
+ * while that function is running.
+ * - All objects passed as input to a library function via a non-const pointer
+ * must not be mutated, inspected, or passed to another library function
+ * while the function is running.
+ * - Furthermore, if a function takes any non-const pointer arguments,
+ * those arguments must not alias one another,
+ * and must not alias any const arguments passed to the function.
+ * - All `const char*` passed as inputs to library functions
+ * are nul-terminated strings.
+ * Additionally, they must be no larger than `SSIZE_MAX`,
+ including the nul.
+ * - If a function takes any mutable pointers
+ **/"""
+
+# This appears "between major sections"
+autogen_warning = "/* Automatically generated by cbindgen. Don't modify manually. */"
+
+# make sure our header can be included in C++.
+cpp_compat = true
+
+# Consistency with Arti.
+tab_width = 8
+
+[defines]
+# This is where we would add mappings from `cfg()` to `#ifdef`.
+# But the only relevant cfg we have is `cfg(feature="ffi")`,
+# which we want to assume is always present if you're using the header.
+
+[export]
+# These structs are not ones we want to expose under their actual names,
+# or ones that we don't want to expose at all.
+exclude = ["RpcErrorCode", "RpcConn", "FfiError", "Utf8CStr"]
+
+[export.rename]
+# Having not declared these structs, we can give them new names in the
+# typedefs that assign them their real names.
+"RpcConn" = "struct ArtiRpcConn"
+"FfiError" = "struct ArtiRpcError"
+"Utf8CStr" = "struct ArtiRpcStr"
+
+[fn]
+# Lay out one argument per line.
+args = "vertical"
+
+[parse]
+
+[parse.expand]
+# We need to run our crate through macro expansion in order to get all
+# of the right functions and constants.
+#
+# (Unfortunately, this requires us to use nightly rust, so that cbindgen
+# can invoke rustc with `-Zunpretty=expanded`.)
+crates = ["arti-rpc-client-core"]
+
+# Run macro-expansion with --all-features so that we see `ffi`.
+all_features = true
diff --git a/crates/arti-rpc-client-core/cbindgen.warnings b/crates/arti-rpc-client-core/cbindgen.warnings
new file mode 100644
index 000000000..4d6df5252
--- /dev/null
+++ b/crates/arti-rpc-client-core/cbindgen.warnings
@@ -0,0 +1,52 @@
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Skip arti-rpc-client-core::_ - (not `pub`).
+WARN: Cannot find a mangling for generic path GenericPath { path: Path { name: "Map" }, export_name: "Map", generics: [Type(Path(GenericPath { path: Path { name: "String" }, export_name: "String", generics: [], ctype: None })), Type(Path(GenericPath { path: Path { name: "Value" }, export_name: "Value", generics: [], ctype: None }))], ctype: None }. This usually means that a type referenced by this generic was incompatible or not found.
+WARN: Can't find RpcConn. This usually means that this type was incompatible or not found.
+WARN: Can't find FfiError. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Can't find RpcErrorCode. This usually means that this type was incompatible or not found.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
+WARN: Missing `[defines]` entry for `feature = "ffi"` in cbindgen config.
diff --git a/crates/arti-rpc-client-core/src/conn.rs b/crates/arti-rpc-client-core/src/conn.rs
index d0f06b609..3f1603d6c 100644
--- a/crates/arti-rpc-client-core/src/conn.rs
+++ b/crates/arti-rpc-client-core/src/conn.rs
@@ -21,6 +21,7 @@ use crate::{
mod auth;
mod connimpl;
+use crate::util::Utf8CString;
pub use connimpl::RpcConn;
/// A handle to an open request.
@@ -43,8 +44,6 @@ pub struct RequestHandle {
//
// I am not at all pleased with these types; we should revise them.
//
-// TODO RPC: Possibly, convert these to hold CString internally.
-//
// TODO RPC: Possibly, all of these should be reconstructed
// from their serde_json::Values rather than forwarded verbatim.
// (But why would we our json to be more canonical than arti's? See #1491.)
@@ -57,9 +56,9 @@ pub struct RequestHandle {
//
// Invariant: it is valid JSON and contains no NUL bytes or newlines.
// TODO RPC: check that the newline invariant is enforced in constructors.
-// TODO RPC consider changing this to CString.
-#[derive(Clone, Debug, derive_more::AsRef)]
-pub struct SuccessResponse(String);
+#[derive(Clone, Debug, derive_more::AsRef, derive_more::Into)]
+#[as_ref(forward)]
+pub struct SuccessResponse(Utf8CString);
/// An Update Response from Arti, with information about the progress of a request.
///
@@ -69,7 +68,8 @@ pub struct SuccessResponse(String);
// TODO RPC: check that the newline invariant is enforced in constructors.
// TODO RPC consider changing this to CString.
#[derive(Clone, Debug, derive_more::AsRef)]
-pub struct UpdateResponse(String);
+#[as_ref(forward)]
+pub struct UpdateResponse(Utf8CString);
/// A Error Response from Arti, indicating that an error occurred.
///
@@ -84,26 +84,33 @@ pub struct UpdateResponse(String);
// Otherwise the `decode` method may panic.
//
// TODO RPC: check that the newline invariant is enforced in constructors.
-// TODO RPC consider changing this to CString.
#[derive(Clone, Debug, derive_more::AsRef)]
+#[as_ref(forward)]
// TODO: If we keep this, it should implement Error.
-pub struct ErrorResponse(String);
+pub struct ErrorResponse(Utf8CString);
impl ErrorResponse {
/// Construct an ErrorResponse from the Error reply.
///
/// This not a From impl because we want it to be crate-internal.
- pub(crate) fn from_validated_string(s: String) -> Self {
+ pub(crate) fn from_validated_string(s: Utf8CString) -> Self {
ErrorResponse(s)
}
/// Try to interpret this response as an [`RpcError`].
pub fn decode(&self) -> RpcError {
- crate::msgs::response::try_decode_response_as_err(&self.0)
+ crate::msgs::response::try_decode_response_as_err(self.0.as_ref())
.expect("Could not decode response that was already decoded as an error?")
.expect("Could not extract error from response that was already decoded as an error?")
}
}
+impl std::fmt::Display for ErrorResponse {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let e = self.decode();
+ write!(f, "Peer said {:?}", e.message())
+ }
+}
+
/// A final response -- that is, the last one that we expect to receive for a request.
///
type FinalResponse = Result<SuccessResponse, ErrorResponse>;
@@ -384,9 +391,6 @@ pub enum ConnectError {
/// IO error while connecting to Arti.
#[error("Unable to make a connection: {0}")]
CannotConnect(Arc<std::io::Error>),
- /// One of our protocol negotiation messages was rejected.
- #[error("Arti rejected our negotiation attempts: {0:?}")]
- NegotiationRejected(ErrorResponse),
/// One of our authentication messages was rejected.
#[error("Arti rejected our authentication: {0:?}")]
AuthenticationRejected(ErrorResponse),
diff --git a/crates/arti-rpc-client-core/src/ffi.rs b/crates/arti-rpc-client-core/src/ffi.rs
new file mode 100644
index 000000000..2dc774212
--- /dev/null
+++ b/crates/arti-rpc-client-core/src/ffi.rs
@@ -0,0 +1,174 @@
+//! Exposed C APIs for arti-rpc-client-core.
+//!
+//! See top-level documentation in header file for C conventions that affect the safety of these functions.
+//! (These include things like "all input pointers must be valid" and so on.)
+
+pub mod err;
+mod util;
+
+use err::{ArtiRpcError, InvalidInput};
+use std::ffi::c_char;
+use util::{ffi_body_raw, ffi_body_with_err, OptOutPtrExt as _, OutPtr};
+
+use crate::{util::Utf8CString, RpcConnBuilder};
+
+/// A status code returned by an Arti RPC function.
+///
+/// On success, a function will return `ARTI_SUCCESS (0)`.
+/// On failure, a function will return some other status code.
+pub type ArtiRpcStatus = u32;
+
+/// An open connection to Arti over an a RPC protocol.
+///
+/// This is a thread-safe type: you may safely use it from multiple threads at once.
+///
+/// Once you are no longer going to use this connection at all, you must free
+/// it with [`arti_rpc_conn_free`]
+pub type ArtiRpcConn = crate::RpcConn;
+
+/// An owned string, returned by this library.
+///
+/// This string must be released with `arti_rpc_str_free`.
+/// You can inspect it with `arti_rpc_str_get`, but you may not modify it.
+/// The string is guaranteed to be UTF-8 and NUL-terminated.
+pub type ArtiRpcStr = Utf8CString;
+
+/// Try to open a new connection to an Arti instance.
+///
+/// The location of the instance and the method to connect to it are described in
+/// `connection_string`.
+///
+/// (TODO RPC: Document the format of this string better!)
+///
+/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*rpc_conn_out` to a new ArtiRpcConn.
+/// Otherwise return some other status code, set `*rpc_conn_out` to NULL, and set
+/// `*error_out` (if provided) to a newly allocated error object.
+///
+///
+/// # Ownership
+///
+/// The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`,
+/// if set, are eventually freed.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_connect(
+ connection_string: *const c_char,
+ rpc_conn_out: *mut *mut ArtiRpcConn,
+ error_out: *mut *mut ArtiRpcError,
+) -> ArtiRpcStatus {
+ ffi_body_with_err!(
+ {
+ let connection_string: Option<&str> [in_str_opt];
+ let rpc_conn_out: Option<OutPtr<ArtiRpcConn>> [out_ptr_opt];
+ err error_out : Option<OutPtr<ArtiRpcError>>;
+ } in {
+ let connection_string = connection_string
+ .ok_or(InvalidInput::NullPointer)?;
+
+ let builder = RpcConnBuilder::from_connect_string(connection_string)?;
+
+ let conn = builder.connect()?;
+
+ rpc_conn_out.write_value_if_ptr_set(conn);
+ }
+ )
+}
+
+/// Run an RPC request over `rpc_conn` and wait for a successful response.
+///
+/// The message `msg` should be a valid RPC request in JSON format.
+/// If you omit its `id` field, one will be generated: this is typically the best way to use this function.
+///
+/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string
+/// containing the JSON response to your request (including `id` and `response` fields).
+///
+/// Otherwise return some other status code, set `*response_out` to NULL,
+/// and set `*error_out` (if provided) to a newly allocated error object.
+///
+/// (If response_out is set to NULL, then any successful response will be ignored.)
+///
+/// # Ownership
+///
+/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_conn_execute(
+ rpc_conn: *const ArtiRpcConn,
+ msg: *const c_char,
+ response_out: *mut *mut ArtiRpcStr,
+ error_out: *mut *mut ArtiRpcError,
+) -> ArtiRpcStatus {
+ ffi_body_with_err!(
+ {
+ let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
+ let msg: Option<&str> [in_str_opt];
+ let response_out: Option<OutPtr<ArtiRpcStr>> [out_ptr_opt];
+ err error_out: Option<OutPtr<ArtiRpcError>>;
+ } in {
+ let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
+ let msg = msg.ok_or(InvalidInput::NullPointer)?;
+
+ let success = rpc_conn.execute(msg)??;
+ response_out.write_value_if_ptr_set(Utf8CString::from(success));
+ }
+ )
+}
+
+/// Free a string returned by the Arti RPC API.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_str_free(string: *mut ArtiRpcStr) {
+ ffi_body_raw!(
+ {
+ let string: Option<Box<ArtiRpcStr>> [in_ptr_consume_opt];
+ } in {
+ drop(string);
+ // Safety: Return value is (); trivially safe.
+ ()
+ }
+ );
+}
+
+/// Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`.
+///
+/// The resulting string is guaranteed to be valid UTF-8.
+///
+/// (Returns NULL if the input is NULL.)
+///
+/// # Correctness requirements
+///
+/// The resulting string pointer is valid only for as long as the input `string` is not freed.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_str_get(string: *const ArtiRpcStr) -> *const c_char {
+ ffi_body_raw!(
+ {
+ let string: Option<&ArtiRpcStr> [in_ptr_opt];
+ } in {
+ // Safety: returned pointer is null, or semantically borrowed from `string`.
+ // It is only null if `string` was null.
+ // The caller is not allowed to modify it.
+ match string {
+ Some(s) => s.as_ptr(),
+ None => std::ptr::null(),
+ }
+
+ }
+ )
+}
+
+/// Close and free an open Arti RPC connection.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_conn_free(rpc_conn: *mut ArtiRpcConn) {
+ ffi_body_raw!(
+ {
+ let rpc_conn: Option<Box<ArtiRpcConn>> [in_ptr_consume_opt];
+ } in {
+ drop(rpc_conn);
+ // Safety: Return value is (); trivially safe.
+ ()
+
+ }
+ );
+}
diff --git a/crates/arti-rpc-client-core/src/ffi/err.rs b/crates/arti-rpc-client-core/src/ffi/err.rs
new file mode 100644
index 000000000..0bad4e442
--- /dev/null
+++ b/crates/arti-rpc-client-core/src/ffi/err.rs
@@ -0,0 +1,436 @@
+//! Error handling logic for our ffi code.
+
+use c_str_macro::c_str;
+use paste::paste;
+use std::ffi::{c_char, CStr};
+use std::fmt::Display;
+use std::panic::{catch_unwind, UnwindSafe};
+
+use crate::conn::ErrorResponse;
+use crate::util::Utf8CString;
+
+use super::util::{ffi_body_raw, OptOutPtrExt as _, OutPtr};
+use super::ArtiRpcStatus;
+
+/// Helper:
+/// Given a restricted enum defining FfiStatus, also define a series of constants for its variants,
+/// and a string conversion function.
+
+// NOTE: I tried to use derive_deftly here, but ran into trouble when defining the constants.
+// I wanted to have them be "pub const ARTI_FOO = FfiStatus::$vname",
+// but that doesn't work with cbindgen, which won't expose a constant unless it is a public type
+// it can recognize.
+// There is no way to use derive_deftly to look at the explicit discriminant of an enum.
+macro_rules! define_ffi_status {
+ {
+ $(#[$tm:meta])*
+ pub(crate) enum FfiStatus {
+ $(
+ $(#[$m:meta])*
+ [$s:expr]
+ $id:ident = $e:expr,
+ )+
+ }
+
+ } => {paste!{
+ $(#[$tm])*
+ pub(crate) enum FfiStatus {
+ $(
+ $(#[$m])*
+ $id = $e,
+ )+
+ }
+
+ $(
+ $(#[$m])*
+ pub const [<ARTI_RPC_STATUS_ $id:snake:upper >] : ArtiRpcStatus = $e;
+ )+
+
+ /// Return a string representing the meaning of a given `ArtiRpcStatus`.
+ ///
+ /// The result will always be non-NULL, even if the status is unrecognized.
+ #[no_mangle]
+ pub extern "C" fn arti_status_to_str(status: ArtiRpcStatus) -> *const c_char {
+ match status {
+ $(
+ [<ARTI_RPC_STATUS_ $id:snake:upper>] => c_str!($s),
+ )+
+ _ => c_str!("(unrecognized status)"),
+ }.as_ptr()
+ }
+ }}
+}
+
+define_ffi_status! {
+/// View of FFI status as rust enumeration.
+///
+/// Not exposed in the FFI interfaces, except via cast to ArtiStatus.
+///
+/// We define this as an enumeration so that we can treat it exhaustively in Rust.
+#[derive(Copy, Clone, Debug)]
+#[repr(u32)]
+pub(crate) enum FfiStatus {
+ /// The function has returned successfully.
+ #[allow(dead_code)]
+ ["Success"]
+ Success = 0,
+
+ /// One or more of the inputs to a library function was invalid.
+ ///
+ /// (This error was generated by the library, before any request was sent.)
+ ["Invalid input"]
+ InvalidInput = 1,
+
+ /// Tried to use some functionality
+ /// (for example, an authentication method or connection scheme)
+ /// that wasn't available on this platform or build.
+ ///
+ /// (This error was generated by the library, before any request was sent.)
+ ["Not supported"]
+ NotSupported = 2,
+
+ /// Tried to connect to Arti, but an IO error occurred.
+ ///
+ /// This may indicate that Arti wasn't running,
+ /// or that Arti was built without RPC support,
+ /// or that Arti wasn't running at the specified location.
+ ///
+ /// (This error was generated by the library.)
+ ["An IO error ocurred while connecting to Arti"]
+ ConnectIo = 3,
+
+ /// We tried to authenticate with Arti, but it rejected our attempt.
+ ///
+ /// (This error was sent by the peer.)
+ ["Authentication rejected"]
+ BadAuth = 4,
+
+ /// Our peer has, in some way, violated the Arti-RPC protocol.
+ ///
+ /// (This error was generated by the library,
+ /// based on a response from Arti that appeared to be invalid.)
+ ["Peer violated the RPC protocol"]
+ PeerProtocolViolation = 5,
+
+ /// The peer has closed our connection; possibly because it is shutting down.
+ ///
+ /// (This error was generated by the library,
+ /// based on the connection being closed or reset from the peer.)
+ ["Peer has shut down"]
+ Shutdown = 6,
+
+ /// An internal error occurred in the arti rpc client.
+ ///
+ /// (This error was generated by the library.
+ /// If you see it, there is probably a bug in the library.)
+ ["Internal error; possible bug?"]
+ Internal = 7,
+
+ /// The peer reports that one of our requests has failed.
+ ///
+ /// (This error was sent by the peer, in response to one of our requests.
+ /// No further responses to that request will be received or accepted.)
+ ["Request has failed"]
+ RequestFailed = 8,
+
+ /// Tried to check the status of a request and found that it was no longer running.
+ ///
+ /// TODO RPC: We should make sure that this is the actual semantics we want for this
+ /// error! Revisit after we have implemented real cancellation.
+ ["Request was cancelled"]
+ RequestCancelled = 9,
+}
+}
+
+/// An error as returned by the Arti FFI code.
+#[derive(Debug, Clone)]
+pub struct FfiError {
+ /// The status of this error messages
+ pub(super) status: ArtiRpcStatus,
+ /// A human-readable message describing this error
+ message: Utf8CString,
+ /// If present, a Json-formatted message from our peer that we are representing with this error.
+ error_response: Option<ErrorResponse>,
+}
+
+impl FfiError {
+ /// Helper: If this error stems from a resoponse from our RPC peer,
+ /// return that reponse.
+ fn error_response_as_ptr(&self) -> Option<*const c_char> {
+ self.error_response.as_ref().map(|response| {
+ let cstr: &CStr = response.as_ref();
+ cstr.as_ptr()
+ })
+ }
+}
+
+/// Convenience trait to help implement `Into<FfiError>`
+///
+/// Any error that implements this trait will be convertible into an [`FfiError`].
+// additional requirements: display doesn't make NULs.
+pub(crate) trait IntoFfiError: Display + Sized {
+ /// Return the status
+ fn status(&self) -> FfiStatus;
+ /// Return a message for this error.
+ ///
+ /// By default, returns the Display of this error.
+ fn message(&self) -> String {
+ self.to_string()
+ }
+ /// Consume this error and return an [`ErrorResponse`]
+ fn into_error_response(self) -> Option<ErrorResponse> {
+ None
+ }
+}
+impl<T: IntoFfiError> From<T> for FfiError {
+ fn from(value: T) -> Self {
+ let status = value.status() as u32;
+ let message = value
+ .message()
+ .try_into()
+ .expect("Error message had a NUL?");
+ let error_response = value.into_error_response();
+ Self {
+ status,
+ message,
+ error_response,
+ }
+ }
+}
+impl From<void::Void> for FfiError {
+ fn from(value: void::Void) -> Self {
+ void::unreachable(value)
+ }
+}
+
+/// Tried to call a ffi function with a not-permitted argument.
+#[derive(Clone, Debug, thiserror::Error)]
+pub(super) enum InvalidInput {
+ /// Tried to convert a NULL pointer to a string.
+ #[error("Provided string was NULL.")]
+ NullPointer,
+
+ /// Tried to convert a non-UTF string.
+ #[error("Provided string was not UTF-8")]
+ BadUtf8,
+}
+
+impl From<void::Void> for InvalidInput {
+ fn from(value: void::Void) -> Self {
+ void::unreachable(value)
+ }
+}
+
+impl IntoFfiError for InvalidInput {
+ fn status(&self) -> FfiStatus {
+ FfiStatus::InvalidInput
+ }
+}
+
+impl IntoFfiError for crate::ConnectError {
+ fn status(&self) -> FfiStatus {
+ use crate::ConnectError as E;
+ use FfiStatus as F;
+ match self {
+ E::SchemeNotSupported => F::NotSupported,
+ E::CannotConnect(_) => F::ConnectIo,
+ E::AuthenticationRejected(_) => F::BadAuth,
+ E::BadMessage(_) => F::PeerProtocolViolation,
+ E::ProtoError(e) => e.status(),
+ }
+ }
+
+ fn into_error_response(self) -> Option<ErrorResponse> {
+ use crate::ConnectError as E;
+ match self {
+ E::AuthenticationRejected(msg) => Some(msg),
+ _ => None,
+ }
+ }
+}
+
+impl IntoFfiError for crate::ProtoError {
+ fn status(&self) -> FfiStatus {
+ use crate::ProtoError as E;
+ use FfiStatus as F;
+ match self {
+ E::Shutdown(_) => F::Shutdown,
+ E::InvalidRequest(_) => F::InvalidInput,
+ E::RequestIdInUse => F::InvalidInput,
+ E::RequestCancelled => F::RequestCancelled,
+ E::DuplicateWait => F::Internal,
+ E::CouldNotEncode(_) => F::Internal,
+ }
+ }
+}
+
+impl IntoFfiError for crate::BuilderError {
+ fn status(&self) -> FfiStatus {
+ use crate::BuilderError as E;
+ use FfiStatus as F;
+ match self {
+ E::InvalidConnectString => F::InvalidInput,
+ }
+ }
+}
+
+impl IntoFfiError for ErrorResponse {
+ fn status(&self) -> FfiStatus {
+ FfiStatus::RequestFailed
+ }
+ fn into_error_response(self) -> Option<ErrorResponse> {
+ Some(self)
+ }
+}
+
+/// An error returned by the Arti RPC code, exposed as an object.
+///
+/// When a function returns an [`ArtiRpcStatus`] other than [`ARTI_RPC_STATUS_SUCCESS`],
+/// it will also expose a newly allocated value of this type
+/// via its `error_out` parameter.
+pub type ArtiRpcError = FfiError;
+
+/// Return the status code associated with a given error.
+///
+/// If `err` is NULL, return [`ARTI_RPC_STATUS_INVALID_INPUT`].
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_err_status(err: *const ArtiRpcError) -> ArtiRpcStatus {
+ ffi_body_raw!(
+ {
+ let err: Option<&ArtiRpcError> [in_ptr_opt];
+ } in {
+ err.map(|e| e.status)
+ .unwrap_or(ARTI_RPC_STATUS_INVALID_INPUT)
+ // Safety: Return value is ArtiRpcStatus; trivially safe.
+ }
+ )
+}
+
+/// Return a human-readable error message associated with a given error.
+///
+/// The format of these messages may change arbitrarily between versions of this library;
+/// it is a mistake to depend on the actual contents of this message.
+///
+/// Return NULL if the input `err` is NULL.
+///
+/// # Correctness requirements
+///
+/// The resulting string pointer is valid only for as long as the input `err` is not freed.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_err_message(err: *const ArtiRpcError) -> *const c_char {
+ ffi_body_raw!(
+ {
+ let err: Option<&ArtiRpcError> [in_ptr_opt];
+ } in {
+ err.map(|e| e.message.as_ptr())
+ .unwrap_or(std::ptr::null())
+ // Safety: returned pointer is null, or semantically borrowed from `err`.
+ // It is only null if `err` was null.
+ // The caller is not allowed to modify it.
+ }
+ )
+}
+
+/// Return a Json-formatted error response associated with a given error.
+///
+/// These messages are full responses, including the `error` field,
+/// and the `id` field (if present).
+///
+/// Return NULL if the specified error does not represent an RPC error response.
+///
+/// Return NULL if the input `err` is NULL.
+///
+/// # Correctness requirements
+///
+/// The resulting string pointer is valid only for as long as the input `err` is not freed.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_err_response(err: *const ArtiRpcError) -> *const c_char {
+ ffi_body_raw!(
+ {
+ let err: Option<&ArtiRpcError> [in_ptr_opt];
+ } in {
+ err.and_then(ArtiRpcError::error_response_as_ptr)
+ .unwrap_or(std::ptr::null())
+ // Safety: returned pointer is null, or semantically borrowed from `err`.
+ // It is only null if `err` was null, or if `err` contained no response field.
+ // The caller is not allowed to modify it.
+ }
+ )
+}
+
+/// Make and return copy of a provided error.
+///
+/// Return NULL if the input is NULL.
+///
+/// # Ownership
+///
+/// The caller is responsible for making sure that the returned object
+/// is eventually freed with `arti_rpc_err_free()`.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_err_clone(err: *const ArtiRpcError) -> *mut ArtiRpcError {
+ ffi_body_raw!(
+ {
+ let err: Option<&ArtiRpcError> [in_ptr_opt];
+ } in {
+ err.map(|e| Box::into_raw(Box::new(e.clone())))
+ .unwrap_or(std::ptr::null_mut())
+ // Safety: returned pointer is null, or newly allocated via Box::new().
+ // It is only null if the input was null.
+ }
+ )
+}
+
+/// Release storage held by a provided error.
+#[allow(clippy::missing_safety_doc)]
+#[no_mangle]
+pub unsafe extern "C" fn arti_rpc_err_free(err: *mut ArtiRpcError) {
+ ffi_body_raw!(
+ {
+ let err: Option<Box<ArtiRpcError>> [in_ptr_consume_opt];
+ } in {
+ drop(err);
+ // Safety: Return value is (); trivially safe.
+ ()
+ }
+ );
+}
+
+/// Run `body` and catch panics. If one occurs, return the result of `on_err` instead.
+///
+/// We wrap the body of every C ffi function with this function
+/// (or with `handle_errors`, which uses this function),
+/// even if we do not think that the body can actually panic.
+pub(super) fn abort_on_panic<F, T>(body: F) -> T
+where
+ F: FnOnce() -> T + UnwindSafe,
+{
+ #[allow(clippy::print_stderr)]
+ match catch_unwind(body) {
+ Ok(x) => x,
+ Err(_panic_info) => {
+ eprintln!("Internal panic in arti-rpc library: aborting!");
+ std::process::abort();
+ }
+ }
+}
+
+/// Call `body`, converting any errors or panics that occur into an FfiError,
+/// and storing that error in `error_out`.
+pub(super) fn handle_errors<F>(error_out: Option<OutPtr<FfiError>>, body: F) -> ArtiRpcStatus
+where
+ F: FnOnce() -> Result<(), FfiError> + UnwindSafe,
+{
+ match abort_on_panic(body) {
+ Ok(()) => ARTI_RPC_STATUS_SUCCESS,
+ Err(e) => {
+ // "body" returned an error.
+ let status = e.status;
+ error_out.write_value_if_ptr_set(e);
+ status
+ }
+ }
+}
diff --git a/crates/arti-rpc-client-core/src/ffi/util.rs b/crates/arti-rpc-client-core/src/ffi/util.rs
new file mode 100644
index 000000000..cf7aec99f
--- /dev/null
+++ b/crates/arti-rpc-client-core/src/ffi/util.rs
@@ -0,0 +1,592 @@
+//! Helpers for working with FFI.
+
+use std::mem::MaybeUninit;
+
+/// Helper for output parameters represented as `*mut *mut T`.
+///
+/// This is for an API which, from a C POV, returns an output via a parameter of type
+/// `Foo **foo_out`. When an `OutPtr` is constructed, `*foo_out` is necessarily non-null.
+///
+/// If `foo_out` is not NULL, then `*foo_out` is always set to NULL when an `OutPtr`
+/// is constructed, so that even if the FFI code panics, the inner pointer will be initialized to
+/// _something_.
+pub(super) struct OutPtr<'a, T>(&'a mut *mut T);
+
+impl<'a, T> OutPtr<'a, T> {
+ /// Construct `Option<Self>` from a possibly NULL pointer; initialize `*ptr` to NULL if possible.
+ ///
+ /// # Safety
+ ///
+ /// The outer pointer, if set, must be valid, and must not alias any other pointers.
+ ///
+ /// See also the requirements on `pointer::as_mut()`.
+ ///
+ /// # No panics!
+ ///
+ /// This method can be invoked in cases where panicking is not allowed (such as
+ /// in a FFI method, outside of `handle_errors()` or `catch_panic()`.)
+ //
+ // (I have tested this using the `no-panic` crate. But `no-panic` is not suitable
+ // for use in production, since it breaks when run in debug mode.)
+ pub(super) unsafe fn from_opt_ptr(ptr: *mut *mut T) -> Option<Self> {
+ if ptr.is_null() {
+ None
+ } else {
+ // TODO: Use `.as_mut_uninit` once it is stable.
+ //
+ // SAFETY: See documentation for [`<*mut *mut T>::as_uninit_mut`]
+ // at https://doc.rust-lang.org/std/primitive.pointer.html#method.as_uninit_mut :
+ // This is the same code.
+ let ptr: &mut MaybeUninit<*mut T> = unsafe { &mut *(ptr as *mut MaybeUninit<*mut T>) };
+ let ptr: &mut *mut T = ptr.write(std::ptr::null_mut());
+ Some(OutPtr(ptr))
+ }
+ }
+
+ /// Consume this OutPtr and the provided value, writing the value into the outptr.
+ pub(super) fn write_value(self, value: T) {
+ // Note that all the unsafety happened when we constructed a &mut from the pointer.
+ //
+ // Note also that this method consumes `self`. That's because we want to avoid multiple
+ // writes to the same OutPtr: If we did that, we would sometimes have to free a previous
+ // value.
+ *self.0 = Box::into_raw(Box::new(value));
+ }
+}
+
+/// Trait to prevent implementation of OptOutPtrExt inappropriately.
+//
+// TODO: Move this into a separate module once our MSRV allows us to do so.
+// With 1.70, it causes an error.
+pub(super) trait Sealed {}
+/// Extension trait on `Option<OutPtr<T>>`
+#[allow(private_bounds)]
+pub(super) trait OptOutPtrExt<T>: Sealed {
+ /// Consume this `Option<OutPtr<T>>` and the provided value.
+ ///
+ /// If this is Some, write the value into the outptr.
+ ///
+ /// Otherwise, discard the value.
+ fn write_value_if_ptr_set(self, value: T);
+}
+impl<'a, T> Sealed for Option<OutPtr<'a, T>> {}
+impl<'a, T> OptOutPtrExt<T> for Option<OutPtr<'a, T>> {
+ fn write_value_if_ptr_set(self, value: T) {
+ if let Some(outptr) = self {
+ outptr.write_value(value);
+ }
+ }
+}
+
+/// Implement the body of an FFI function.
+///
+/// This macro handles the calling convention of an FFI function.
+/// Proper use of this macro will ensure that the FFI function behaves as documented,
+/// as regards pointer handling, ownership, lifetimes, and error handling.
+/// It also catches panics, making sure that we don't unwind into the FFI caller.
+/// I.e. it ensures that correct callers will not experience UB.
+///
+/// This variant is for functions that
+/// don't pass back an `ArtiRpcError` via an out parameter.
+/// See [`ffi_body_with_err!`] for that.
+///
+/// This macro is meant to be invoked as follows:
+///
+/// ```ignore
+/// ffi_body_raw!(
+/// {
+/// [CONVERSIONS]
+/// } in {
+/// [BODY]
+/// } on invalid {
+/// [VALUE_ON_BAD_INPUT]
+/// }
+/// )
+/// ```
+///
+/// For example:
+///
+/// ```ignore
+/// pub extern "C" fn arti_rpc_cook_meal(
+/// recipe: *const Recipe,
+/// special_ingredients: *const Ingredients,
+/// n_guests: usize,
+/// dietary_constraints: *const c_char,
+/// food_out: *mut *mut DeliciousMeal,
+/// ) -> usize {
+/// ffi_body_raw!(
+/// { // [CONVERSIONS]
+/// let recipe: Option<&Recipe> [in_ptr_opt];
+/// let ingredients: Option<&Ingredients> [in_ptr_opt];
+/// let dietary_constraints: Option<&str> [in_str_opt];
+/// let food_out: OutPtr<DeliciousMeal> [out_ptr_opt];
+/// } in {
+/// // [BODY]
+/// let Some(recipe) = recipe else { return 0 };
+/// let delicious_meal = prepare_meal(
+/// recipe, ingredients, dietary_constraints, n_guests
+/// );
+/// food_out.write_value_if_nonnull(delicious_meal);
+/// n_guests
+/// } on invalid {
+/// // [VALUE_ON_BAD_INPUT]
+/// 0
+/// }
+/// )
+/// }
+/// ```
+///
+/// The first part (`CONVERSIONS`) defines a set of conversions to be done on the function inputs.
+/// These are documented below.
+/// Each conversion performs an unsafe operation,
+/// making certain assumptions about an input variable,
+/// in order to produce an output of the specified type.
+/// Conversions can reject input values.
+/// If they do, the function will return;
+/// see discussion of `[VALUE_ON_BAD_INPUT]`
+///
+/// Pointer parameters to the outer function *must not be ignored*.
+/// Every raw pointer parameter must be processed by this macro.
+/// (For raw pointer arguments that are not,
+/// no guarantees are made by the macro,
+/// and the overall function will probably be unsound.
+/// There is no checking that every pointer parameter is properly used,
+/// other than Rust's usual detection of unused variables.)
+///
+/// The second part (`BODY`) is the body of the function.
+/// The body is *outside* `unsafe`, and
+/// it should generally be possible to write this body without using unsafe code.
+/// The result of this block is the returned value of the function.
+///
+/// The third part (`VALUE_ON_BAD_INPUT`) is an expression to be returned
+/// as the result of the function if any input pointer has a rejected value.
+/// You may omit the entire `on invalid { ... }` part of the macro's input
+/// when all of the conversions are infallible.
+/// (This is checked statically.)
+///
+/// ## Supported conversions
+///
+/// All conversions take the following format:
+///
+/// `let NAME : TYPE [METHOD] ;`
+///
+/// The `NAME` must match one of the inputs to the function.
+///
+/// The `TYPE` must match the actual type that the input will be converted to.
+/// (These types are generally easy to use ergonomically from safe rust.)
+///
+/// The `METHOD` is an identifier explaining how the input is to be converted.
+///
+/// The following methods are recognized:
+///
+/// | method | input type | converted to | can reject input? |
+/// |----------------------|-----------------|--------------------|-------------------|
+/// | `in_ptr_opt` | `*const T` | `Option<&T>` | N |
+/// | `in_str_opt` | `*const c_char` | `Option<&str>` | Y |
+/// | `in_ptr_consume_opt` | `*mut T` | `Option<Box<T>>` | N |
+/// | `out_ptr_opt` | `*mut *mut T` | `Option<OutPtr<T>>`| N |
+///
+/// > (Note: Other conversion methods are logically possible, but have not been added yet,
+/// > since they would not yet be used in this crate.)
+///
+/// ## Safety
+///
+/// The `in_ptr_opt` method
+/// has the safety requirements of
+/// [`<*const T>::as_ref`](https://doc.rust-lang.org/std/primitive.pointer.html#method.as_ref).
+/// Informally, this means:
+/// * If the pointer is not null, it must point
+/// to a valid aligned dereferenceable instance of `T`.
+/// * The underlying `T` must not be freed or modified for so long as the function is running.
+///
+/// The `in_str_opt` method, when its input is non-NULL,
+/// has the safety requirements of [`CStr::from_ptr`](std::ffi::CStr::from_ptr).
+/// Informally, this means:
+/// * If the pointer is not null, it must point to a nul-terminated string.
+/// * The string must not be freed or modified for so long as the function is running.
+///
+/// Additionally, the `[in_str_opt]` method
+/// will detect invalid any string that is not UTF-8.
+///
+/// The `in_ptr_consume_opt` method, when its input is non-NULL,
+/// has the safety requirements of [`Box::from_raw`].
+/// Informally, this is satisfied when:
+/// * If the pointer is not null, it should be
+/// the result of an earlier a call to `Box<T>::into_raw`.
+/// (Note that using either `out_ptr_*` method
+/// will output pointers that can later be consumed in this way.)
+///
+/// The `out_ptr_opt` method
+/// has the safety requirements of
+/// [`<*mut *mut T>::as_uninit_mut`](https://doc.rust-lang.org/std/primitive.pointer.html#method.as_uninit_mut).
+/// Informally, this means:
+/// * If the pointer (call it "out") is non-NULL, then `*out` must point to aligned
+/// "dereferenceable" (q.v.) memory holding a possibly uninitialized "*mut T".
+///
+/// (Note that immediately upon conversion, if `out` is non-NULL,
+/// `*out` is set to NULL. See documentation for `OptPtr`.)
+///
+/// The return value of `BODY` becomes the return value of the C FFI function.
+/// It is the macro user's responsibility to ensure
+/// that it conforms to the published API.
+/// For example, if the return value is a raw pointer,
+/// the macro user must ensure it's suitably dereferencable,
+/// that its lifetime is documented,
+/// and only null when the API says that's allowed.
+//
+// Design notes:
+// - I am keeping the conversions separate from the body below, since we don't want to catch
+// InvalidInput from the body.
+// - The "on invalid" value must be specified explicitly if it can happen,
+// since in general we should force the caller to think about it.
+// Getting a 0 or -1 wrong here can have nasty results.
+// - The conversion syntax deliberately includes the type of the converted argument,
+// on the theory that it makes the functions more readable.
+// - The conversion code deliberately shadows the original parameter with the
+// converted parameter.
+macro_rules! ffi_body_raw {
+ {
+ {
+ $(
+ let $name:ident : $type:ty [$how:ident]
+ );*
+ $(;)?
+ } in {
+ $($body:tt)+
+ } on invalid {
+ $err:expr
+ }
+ } => {
+ crate::ffi::err::abort_on_panic(|| {
+ // run conversions and check for invalid input exceptions.
+ crate::ffi::util::ffi_initialize!{
+ {
+ $( let $name : $type [$how]; )*
+ } else with _ignore_err : crate::ffi::err::InvalidInput {
+ #[allow(clippy::unused_unit)]
+ return $err;
+ }
+ };
+
+ $($body)+
+
+ },
+ )
+ };
+
+ {
+ {
+ $(
+ let $name:ident : $type:ty [$how:ident]
+ );*
+ $(;)?
+ } in {
+ $($body:tt)+
+ }
+ } => {
+ crate::ffi::err::abort_on_panic(|| {
+ // run conversions and check for invalid input exceptions.
+ crate::ffi::util::ffi_initialize!{
+ {
+ $( let $name : $type [$how]; )*
+ } else with impossible_error : void::Void {
+ void::unreachable(impossible_error);
+ }
+ };
+
+ $($body)+
+
+ },
+ )
+ };
+
+}
+pub(super) use ffi_body_raw;
+
+/// Implement the body of an FFI function that returns an ArtiRpcStatus.
+///
+/// This macro is meant to be invoked as follows:
+/// ```text
+/// ffi_body_with_err!(
+/// {
+/// [CONVERSIONS]
+/// err [ERRNAME] : OutPtr<ArtiRpcError>;
+/// } in {
+/// [BODY]
+/// }
+/// })```
+///
+/// For example:
+///
+/// ```ignore
+/// pub extern "C" fn arti_rpc_wombat_feed(
+/// wombat: *const Wombat,
+/// wombat_chow: *const Meal,
+/// error_out: *mut *mut ArtiRpcError
+/// ) -> ArtiRpcStatus {
+/// ffi_body_with_err!(
+/// {
+/// let wombat: Option<&Wombat> [in_ptr_opt];
+/// let wombat_chow: Option<&Meal> [in_ptr_opt];
+/// err error_out: Option<OutPtr<ArtiRpcError>>;
+/// } in {
+/// let wombat = wombat.ok_or(InvalidInput::NullPointer)?
+/// let wombat_chow = wombat_chow.ok_or(InvalidInput::NullPointer)?
+/// wombat.please_enjoy(wombat_chow)?;
+/// }
+/// )
+/// }
+/// ```
+///
+/// The resulting function has the same kinds
+/// of conversions as would [`ffi_body_raw!`].
+///
+/// The differences are:
+/// * Instead of returning a value, the body can only give errors with `?`.
+/// * The function must return ArtiRpcStatus.
+/// * Any errors that occur during the conversions or the body
+/// are converted into an ArtiRpcError,
+/// and given to the user via `error_out` if it is non-NULL.
+/// A corresponding ArtiRpcStatus is returned.
+///
+/// ## Safety
+///
+/// The safety requirements are the same as for `ffi_body_raw`, except that:
+///
+/// The safety requirements for the `err` conversion
+/// are the same as those for `out_ptr_opt` (q.v.).
+///
+/// `ffi_body_with_err` then additionally ensures conformance of
+/// the return value with the API's error handling rules.
+macro_rules! ffi_body_with_err {
+ {
+ {
+ $(
+ let $name:ident : $type:ty [$how:ident];
+ )*
+ err $err_out:ident : $err_type:ty $(;)?
+ } in {
+ $($body:tt)+
+ }
+ } => {{
+ use void::ResultVoidExt as _;
+ let $err_out: $err_type =
+ unsafe { crate::ffi::util::arg_conversion::out_ptr_opt($err_out) }
+ .void_unwrap();
+
+ crate::ffi::err::handle_errors($err_out,
+ || {
+ crate::ffi::util::ffi_initialize!{
+ {
+ $( let $name : $type [$how]; )*
+ } else with err: crate::ffi::err::ArtiRpcError {
+ return Err(crate::ffi::err::ArtiRpcError::from(err));
+ }
+ };
+
+ let () = { $($body)+ };
+
+ Ok(())
+ }
+ )
+ }}
+}
+pub(super) use ffi_body_with_err;
+
+/// Implement a set of conversions, trying each one.
+///
+/// (It's important that this cannot exit early,
+/// since some conversions have side effects: notably, the ones that create an OutPtr
+/// can initialize that pointer to NULL, and we want to do that unconditionally.
+///
+/// If any conversion fails, run `return ($on_invalid)(error)`
+/// _after_ every conversion has succeeded or failed.
+///
+/// The syntax is:
+///
+/// ```ignore
+/// ffi_initialize!{
+/// { [CONVERSIONS] }
+/// else with [ERR_IDENT] { [ERR_BODY] }
+/// }
+/// ```
+///
+/// The `[CONVERSIONS]` have the same syntax and behavior as in [`ffi_body_raw!`].
+/// After every conversion has been tried, if one or more of them failed,
+/// then the `[ERR_BODY]` code is run,
+/// with `[ERR_IDENT]` bound to an instance of `InvalidInput`.
+macro_rules! ffi_initialize {
+ {
+ {
+ $( let $name:ident : $type:ty [$how:ident] ; )*
+ } else with $err_id:ident: $err_type:ty {
+ $($on_invalid:tt)*
+ }
+ } => {
+ // General approach
+ //
+ // First, we process each `$name` into `Result<$type>`, without doing any early exits.
+ // This ensures that we process every `$name`, even if some of the processing fails.
+ //
+ // Then we convert each `Result<X>` into just `X`
+ // (with an IEFE that returns a `Result<(X,...)>` - one `Result` with a big tuple.
+ // We rebinding the `$name`'s to the values from the tuple.
+ #[allow(unused_parens)]
+ let ($($name,)*) : ($($type,)*) = {
+ $(
+ let $name : Result<$type, _>
+ = unsafe { crate::ffi::util::arg_conversion::$how($name) };
+ )*
+
+ #[allow(clippy::needless_question_mark)]
+ // Note that the question marks here exit from _this_ closure.
+ match (|| -> Result<_,$err_type> {
+ Ok(($($name?,)*))
+ })() {
+ Ok(v) => v,
+ Err($err_id) => {
+ $($on_invalid)*
+ }
+ }
+ };
+ };
+}
+
+/// Functions to implement argument conversion.
+///
+/// Each of these functions corresponds to a conversion mode used in `ffi_initialize!`.
+///
+/// Every function has all of these properties:
+///
+/// - It returns `Err(InvalidInput)` if the conversion fails,
+/// and `Ok($ty)` if the conversion succeeds.
+/// (Infallible conversions always return `Ok`.)
+///
+/// Nothing outside of the `ffi_initialize!` macro should actually invoke these functions!
+#[allow(clippy::unnecessary_wraps)]
+pub(super) mod arg_conversion {
+ use super::OutPtr;
+ use crate::ffi::err::InvalidInput;
+ use std::ffi::{c_char, CStr};
+ use void::Void;
+
+ /// Try to convert a const pointer to an optional reference.
+ ///
+ /// A null pointer is allowed, and converted to `None`.
+ ///
+ /// # Safety
+ ///
+ /// As for [`<*const T>::as_ref`](https://doc.rust-lang.org/std/primitive.pointer.html#method.as_ref).
+ pub(in crate::ffi) unsafe fn in_ptr_opt<'a, T>(input: *const T) -> Result<Option<&'a T>, Void> {
+ Ok(unsafe { input.as_ref() })
+ }
+
+ /// Try to convert a `const char *` to a `&str`.
+ ///
+ /// A null pointer is allowed, and converted to `None`.
+ /// Non-UTF-8 inputs will give an error.
+ ///
+ /// # Safety
+ ///
+ /// As for [`CStr::from_ptr`](std::ffi::CStr::from_ptr).
+ pub(in crate::ffi) unsafe fn in_str_opt<'a>(
+ input: *const c_char,
+ ) -> Result<Option<&'a str>, InvalidInput> {
+ if input.is_null() {
+ return Ok(None);
+ }
+
+ // Safety: We require that the safety properties of CStr::from_ptr hold.
+ unsafe { CStr::from_ptr(input) }
+ .to_str()
+ .map(Some)
+ .map_err(|_| InvalidInput::BadUtf8)
+ }
+
+ /// Try to convert a mutable pointer to a `Option<Box<T>>`.
+ ///
+ /// A null pointer is allowed, and converted to `None`.
+ ///
+ /// # Safety
+ ///
+ /// As for [`Box::from_raw`].
+ pub(in crate::ffi) unsafe fn in_ptr_consume_opt<T>(
+ input: *mut T,
+ ) -> Result<Option<Box<T>>, Void> {
+ Ok(if input.is_null() {
+ None
+ } else {
+ Some(unsafe { Box::from_raw(input) })
+ })
+ }
+
+ /// Try to convert a mutable pointer-to-pointer into an `Option<OutPtr<T>>`.
+ ///
+ /// A null pointer is allowed, and converted into None.
+ ///
+ /// Whatever the target of the original pointer (`input: *mut *mut T`), if `input` is non-null.
+ /// then `*input` is initialized to NULL.
+ ///
+ /// It is safe for `*input` to be uninitialized.
+ ///
+ /// # Safety
+ ///
+ /// As for
+ /// [`<*mut *mut T>::as_uninit_mut`](https://doc.rust-lang.org/std/primitive.pointer.html#method.as_uninit_mut).
+ pub(in crate::ffi) unsafe fn out_ptr_opt<'a, T>(
+ input: *mut *mut T,
+ ) -> Result<Option<OutPtr<'a, T>>, Void> {
+ Ok(unsafe { crate::ffi::util::OutPtr::from_opt_ptr(input) })
+ }
+}
+
+pub(super) use ffi_initialize;
+
+#[cfg(test)]
+mod test {
+ // @@ begin test lint list maintained by maint/add_warning @@
+ #![allow(clippy::bool_assert_comparison)]
+ #![allow(clippy::clone_on_copy)]
+ #![allow(clippy::dbg_macro)]
+ #![allow(clippy::mixed_attributes_style)]
+ #![allow(clippy::print_stderr)]
+ #![allow(clippy::print_stdout)]
+ #![allow(clippy::single_char_pattern)]
+ #![allow(clippy::unwrap_used)]
+ #![allow(clippy::unchecked_duration_subtraction)]
+ #![allow(clippy::useless_vec)]
+ #![allow(clippy::needless_pass_by_value)]
+ //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
+
+ use super::*;
+
+ unsafe fn outptr_user(ptr: *mut *mut i8, set_to_val: Option<i8>) {
+ let ptr = unsafe { OutPtr::from_opt_ptr(ptr) };
+
+ if let Some(v) = set_to_val {
+ ptr.write_value_if_ptr_set(v);
+ }
+ }
+
+ #[test]
+ fn outptr() {
+ let mut ptr_to_int: *mut i8 = 7 as _; // This is a junk dangling pointer. It will get overwritten.
+
+ // Case 1: Don't set to anything.
+ unsafe { outptr_user(&mut ptr_to_int as _, None) };
+ assert!(ptr_to_int.is_null());
+
+ // Cases 2, 3: Provide a null pointer for the output pointer.
+ ptr_to_int = 7 as _; // make it junk again.
+ unsafe { outptr_user(std::ptr::null_mut(), None) };
+ assert_eq!(ptr_to_int, 7 as _); // we didn't pass this in, so it wasn't set.
+ unsafe { outptr_user(std::ptr::null_mut(), Some(5)) };
+ assert_eq!(ptr_to_int, 7 as _); // we didn't pass this in, so it wasn't set.
+
+ // Case 4: Actually set something.
+ unsafe { outptr_user(&mut ptr_to_int as _, Some(123)) };
+ assert!(!ptr_to_int.is_null());
+ let boxed = unsafe { Box::from_raw(ptr_to_int) };
+ assert_eq!(*boxed, 123);
+ }
+}
diff --git a/crates/arti-rpc-client-core/src/lib.rs b/crates/arti-rpc-client-core/src/lib.rs
index 615b9d403..b919042cd 100644
--- a/crates/arti-rpc-client-core/src/lib.rs
+++ b/crates/arti-rpc-client-core/src/lib.rs
@@ -39,8 +39,13 @@
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
-//!
+
+// TODO RPC: Possibly add this to our big list of lints.
+#![deny(unsafe_op_in_unsafe_fn)]
+
mod conn;
+#[cfg(feature = "ffi")]
+pub mod ffi;
pub mod llconn;
mod msgs;
#[macro_use]
diff --git a/crates/arti-rpc-client-core/src/msgs/response.rs b/crates/arti-rpc-client-core/src/msgs/response.rs
index c89cfbc7c..fe86e5d62 100644
--- a/crates/arti-rpc-client-core/src/msgs/response.rs
+++ b/crates/arti-rpc-client-core/src/msgs/response.rs
@@ -5,7 +5,10 @@ use std::sync::Arc;
use serde::Deserialize;
use super::AnyRequestId;
-use crate::{conn::ErrorResponse, util::define_from_for_arc};
+use crate::{
+ conn::ErrorResponse,
+ util::{define_from_for_arc, Utf8CString},
+};
/// An unparsed and unvalidated response, as received from Arti.
///
@@ -29,7 +32,7 @@ impl UnparsedResponse {
#[derive(Clone, Debug)]
pub(crate) struct ValidatedResponse {
/// The text of this response.
- pub(crate) msg: String,
+ pub(crate) msg: Utf8CString,
/// The metadata from this response.
pub(crate) meta: ResponseMeta,
}
@@ -58,10 +61,11 @@ impl UnparsedResponse {
/// return it as a ValidatedResponse.
pub(crate) fn try_validate(self) -> Result<ValidatedResponse, DecodeResponseError> {
let meta = response_meta(self.as_ref())?;
- Ok(ValidatedResponse {
- msg: self.msg,
- meta,
- })
+ let msg = self.msg.try_into().map_err(|_| {
+ // (This should be impossible; serde_json rejects NULs.)
+ DecodeResponseError::ProtocolViolation("Unexpected NUL in validated message")
+ })?;
+ Ok(ValidatedResponse { msg, meta })
}
}
@@ -81,12 +85,6 @@ impl ValidatedResponse {
}
}
-impl From<ValidatedResponse> for String {
- fn from(value: ValidatedResponse) -> Self {
- value.msg
- }
-}
-
/// Metadata extracted from a response while decoding it.
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
@@ -158,7 +156,13 @@ pub(crate) fn response_meta(s: &str) -> Result<ResponseMeta, DecodeResponseError
let ResponseMetaDe { id, body } = serde_json::from_str(s)?;
match (id, body) {
(None, Body::Error(_ignore)) => {
- Err(E::Fatal(ErrorResponse::from_validated_string(s.to_owned())))
+ let msg = s.to_owned().try_into().map_err(|_| {
+ // (This should be impossible; serde_json rejects NULs.)
+ DecodeResponseError::ProtocolViolation(
+ "Unexpected NUL in validated fatal error message",
+ )
+ })?;
+ Err(E::Fatal(ErrorResponse::from_validated_string(msg)))
}
(None, _) => Err(E::ProtocolViolation("Missing ID field")),
(Some(id), body) => Ok(ResponseMeta {
diff --git a/crates/arti-rpc-client-core/src/util.rs b/crates/arti-rpc-client-core/src/util.rs
index 3d22d097d..33d15f425 100644
--- a/crates/arti-rpc-client-core/src/util.rs
+++ b/crates/arti-rpc-client-core/src/util.rs
@@ -14,4 +14,64 @@ macro_rules! define_from_for_arc {
}
};
}
+use std::ffi::{CStr, CString, NulError};
+
pub(crate) use define_from_for_arc;
+
+/// A string that is guaranteed to be UTF-8 and NUL-terminated,
+/// for fast access as either type.
+//
+// TODO RPC: Rename so we can expose it more sensibly.
+#[derive(Clone, Debug)]
+pub struct Utf8CString {
+ /// The body of this string.
+ ///
+ /// # Safety
+ ///
+ /// INVARIANT: This string must be valid UTF-8.
+ ///
+ /// (We do not _yet_ depend on this invariant for safety in our rust code, but we do promise in
+ /// our C ffi that it will hold.)
+ string: Box<CStr>,
+}
+
+impl AsRef<CStr> for Utf8CString {
+ fn as_ref(&self) -> &CStr {
+ &self.string
+ }
+}
+
+impl AsRef<str> for Utf8CString {
+ fn as_ref(&self) -> &str {
+ // TODO: We might someday decide to implement this using unsafe methods, to avoid walking
+ // over the string to enforce properties that are already there.
+ self.string.to_str().expect("Utf8CString was not UTF-8‽")
+ }
+}
+
+// TODO: In theory we could have an unchecked version of this function, if we are 100%
+// sure that serde_json will reject every string that contains a NUL. But let's not do
+// that unless the NUL check shows up in profiles.
+impl TryFrom<String> for Utf8CString {
+ type Error = NulError;
+
+ fn try_from(value: String) -> Result<Self, Self::Error> {
+ // Safety: Since `value` is a `String`, it is guaranteed to be UTF-8.
+ Ok(Utf8CString {
+ string: CString::new(value)?.into_boxed_c_str(),
+ })
+ }
+}
+
+/// Ffi-related functionality for Utf8CStr
+#[cfg(feature = "ffi")]
+pub(crate) mod ffi {
+ use std::ffi::c_char;
+
+ impl super::Utf8CString {
+ /// Expose this Utf8CStr as a C string.
+ pub(crate) fn as_ptr(&self) -> *const c_char {
+ self.string.as_ptr()
+ }
+ }
+}