From 735e64fb9de3e0b22ca1e17dd2990a537d1d6450 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Wed, 22 Jan 2025 14:24:48 -0700 Subject: [PATCH 01/15] Change the way status messages are obtained. This replaces the allocating `strup_message` mechanism with an API that is guaranteed to never allocate memory. This is based on the new Boost.System APIs that accept a scratch buffer in order to do dynamic message generation. --- docs/conf.py | 1 + docs/how-to/communicate.example.c | 5 +- docs/how-to/looping.example.c | 3 +- docs/learn/connect.example.c | 3 +- docs/ref/bson/mut.rst | 6 +-- docs/ref/glossary.rst | 10 ++++ docs/ref/status.rst | 73 ++++++++++++++++++++++----- docs/ref/str.rst | 2 +- include/amongoc/status.h | 26 +++++++--- src/amongoc/collection.cpp | 24 ++++----- src/amongoc/status.c | 1 + src/amongoc/status.cpp | 82 ++++++++++++++++++++++--------- 12 files changed, 173 insertions(+), 63 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b5638a0..b455450 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -229,6 +229,7 @@ def generate_sphinx_inventory( .. |static| replace:: :cpp:`static` .. |const| replace:: :cpp:`const` .. |void| replace:: :cpp:`void` +.. |char| replace:: :cpp:`char` .. |A| replace:: :math:`A` .. |A'| replace:: :math:`A'` .. |B| replace:: :math:`B` diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index d81d31a..c2f5590 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -99,9 +99,8 @@ int main(int argc, char const* const* argv) { amongoc_default_loop_destroy(&loop); if (amongoc_is_error(fin_status)) { - char* m = amongoc_status_strdup_message(fin_status); - fprintf(stderr, "An error occurred: %s\n", m); - free(m); + amongoc_declmsg(msg, fin_status); + fprintf(stderr, "An error occurred: %s\n", msg); return 2; } else { printf("Okay\n"); diff --git a/docs/how-to/looping.example.c b/docs/how-to/looping.example.c index 3780a31..d3791b4 100644 --- a/docs/how-to/looping.example.c +++ b/docs/how-to/looping.example.c @@ -78,9 +78,8 @@ int main(int argc, char const* const* argv) { amongoc_default_loop_destroy(&loop); if (amongoc_is_error(status)) { - char* msg = amongoc_status_strdup_message(status); + amongoc_declmsg(msg, status); fprintf(stderr, "error: %s\n", msg); - free(msg); amongoc_box_destroy(result); return 2; } else { diff --git a/docs/learn/connect.example.c b/docs/learn/connect.example.c index 1dd467b..458247c 100644 --- a/docs/learn/connect.example.c +++ b/docs/learn/connect.example.c @@ -28,9 +28,8 @@ amongoc_box on_connect(amongoc_box userdata, amongoc_status* status, amongoc_box (void)userdata; // Check for an error if (amongoc_is_error(*status)) { - char* msg = amongoc_status_strdup_message(*status); + amongoc_declmsg(msg, *status); fprintf(stderr, "Error while connecting to server: %s\n", msg); - free(msg); } else { printf("Successfully connected!\n"); amongoc_client* client; diff --git a/docs/ref/bson/mut.rst b/docs/ref/bson/mut.rst index cc64dfe..ade0762 100644 --- a/docs/ref/bson/mut.rst +++ b/docs/ref/bson/mut.rst @@ -313,9 +313,9 @@ Utilities .. function:: bson_u32_string bson_u32_string_create(uint32_t i) - Create a small C string representing the base-10 encoding of the given 32-bit - integer `i`. The string is not dynamically allocated, so no deallocation is - necessary. The character array in the returned small string is + Create a small :term:`C string` representing the base-10 encoding of the given + 32-bit integer `i`. The string is not dynamically allocated, so no + deallocation is necessary. The character array in the returned small string is null-terminated. diff --git a/docs/ref/glossary.rst b/docs/ref/glossary.rst index 7548216..fc0ef26 100644 --- a/docs/ref/glossary.rst +++ b/docs/ref/glossary.rst @@ -73,6 +73,16 @@ Terminology This contrasts with :term:`function-like macros `. + C string + + A *C string* is a contiguous array of |char| that is terminated with a nul + character (a |char| with integral value :cpp:`0`). A C string is typically + represented with a pointer-to-|char| in C code, where the pointer refers to + the first character in the character array. The "length" of a *C string* is + the number of characters in the array that precede the nul terminator. i.e. + a C string of length 3 will have three non-zero characters and one nul + character, requiring an array of length 4. + translation unit In C and C++, a *translation unit* consists of the total textual input given diff --git a/docs/ref/status.rst b/docs/ref/status.rst index 499d827..9ce50b8 100644 --- a/docs/ref/status.rst +++ b/docs/ref/status.rst @@ -60,7 +60,7 @@ Types .. function:: std__string message() const noexcept - :C API: `amongoc_status_strdup_message` + :C API: `amongoc_message` .. function:: bool is_error() const noexcept @@ -112,14 +112,43 @@ Functions & Macros `amongoc_status::category`. -.. function:: char* amongoc_status_strdup_message(amongoc_status) +.. function:: const char* amongoc_message(amongoc_status st, char* buf, size_t buflen) - Obtain a *dynamically allocated* C string that describes the status in - human-readable form. + Obtain a human-readable message describing the status `st`. - .. important:: The returned string must be freed with ``free()`` + :param st: The status to inspect. + :param buf: Pointer to a modifiable |char| array of at least `buflen` |char|\ s. + This argument may be a null pointer if `buflen` is zero. + :param buflen: The length of the |char| array pointed-to by `buf`, or zero + if `buf` is a null pointer. + :return: A non-null pointer |S| to a :term:`C string`. - :C++ API: `amongoc_status::message` + The buffer `buf` *may* be used by this function as storage for a + dynamically-generated message string, but the function is not required to + modify `buf`. The returned pointer |S| is never null, and may or may not be + equal to `buf`. + + This function does not dynamically allocate any memory. + + .. seealso:: :c:macro:`amongoc_declmsg` for concisely obtaining the message + from a status object. + + +.. c:macro:: amongoc_declmsg(MsgVar, Status) + + This statement-like macro will obtain the status message :term:`C string` from + the given status ``Status`` and place it in a variable identified by + ``MsgVar``. + + :param MsgVar: Must be an identifier. This macro will declare a variable of + type ``const char*`` with this name, which will contain the message from + ``Status``. + :param Status: Any expression of type `amongoc_status`. + + This macro is a shorthand for the following:: + + char __buffer[128]; + const char* MsgVar = amongoc_message(Status, __buffer, sizeof __buffer) .. var:: const amongoc_status amongoc_okay @@ -138,6 +167,8 @@ Status Categories status codes. The following "methods" are actually function pointers that may be customized by the user to provide new status code behaviors. + .. |the-code| replace:: The integer status code from `amongoc_status::code` + .. rubric:: Customization Points .. function:: const char* name() @@ -145,14 +176,32 @@ Status Categories :return: Must return a statically-allocated null-terminated string that uniquely identifies the category. - .. function:: char* strdup_message(int code) + .. function:: const char* message(int code, char* buf, size_t buflen) - .. |the-code| replace:: The integer status code from `amongoc_status::code` + .. seealso:: User code should use `amongoc_message` instead of calling this function directly. :param code: |the-code| - :return: Must return a dynamically allocated null-terminated string that - describes the status in a human-readable format. The returned string will - be freed with ``free()``. + :param buf: Pointer to an array of |char| at least `buflen` long. This may be null + if `buflen` is zero. + :param buflen: The length of the character array pointed-to by `buf`. If this + is zero, then `buf` may be a null pointer. + :return: Should return a pointer to a :term:`C string` that provides a + human-readable message describing the status code `code`. May return a null + pointer if there is a failure to generate the message text. + + A valid implementation of `message` should do the following: + + 1. If the message for `code` is a statically allocated :term:`C string` |S|, + return |S| without inspecting `buf`. + 2. If the message |M| needs to be dynamically generated and `buf` is not + null, generate the message string in `buf`, ensuring that `buf` contains + a nul terminator at :expr:`buf[buflen-1]` (use of ``snprintf`` is + recommended). Return `buf`. + 3. Otherwise, return a fallback message string or a null pointer. + + If this function returns a null pointer, then `amongoc_message` will replace + it with a fallback message telling the caller that the message text is + unavailable. .. function:: bool is_error(int code) [[optional]] @@ -264,7 +313,7 @@ Built-In |amongoc| Categories source. In this case, no status messages or status semantics are defined, except that `amongoc_is_error` returns ``false`` only if the `amongoc_status::code` is ``0``. - The message returned from `amongoc_status_strdup_message` will always be + The message returned from `amongoc_message` will always be "``amongoc.unknown:``" where ```` is the numeric value of the error code. diff --git a/docs/ref/str.rst b/docs/ref/str.rst index 3160a5f..dbde02a 100644 --- a/docs/ref/str.rst +++ b/docs/ref/str.rst @@ -112,7 +112,7 @@ Types - `mlib_str_view` - `mlib_str` - `mlib_str_mut` - - :cpp:`char [const]*` (null terminated C strings, inluding string literals) + - :cpp:`char [const]*` (:term:`C string`\ s, inluding string literals) From C++ code, any type convertible to `std__string_view` may be used. diff --git a/include/amongoc/status.h b/include/amongoc/status.h index 8c137ed..ef5a07d 100644 --- a/include/amongoc/status.h +++ b/include/amongoc/status.h @@ -20,8 +20,8 @@ mlib_extern_c_begin(); struct amongoc_status_category_vtable { // Get the name of the category const char* (*name)(void); - // Dynamically allocate a new string for the message contained in the status - char* (*strdup_message)(int code); + // Obtain a human-readable message for the status + const char* (*message)(int code, char* obuf, size_t buflen); // Test whether a particular integer value is an error bool (*is_error)(int code); // Test whether a particular integer value represents cancellation @@ -830,13 +830,27 @@ static inline bool amongoc_is_timeout(amongoc_status st) mlib_noexcept { /** * @brief Obtain a human-readable message describing the given status * - * @return char* A dynamically allocated null-terminated C string describing the status. - * @note The returned string must be freed with free()! + * @param st The status to be inspected + * @param buf Pointer to a modifyable character array of length `buflen`, or `NULL` + * if `buflen` is zero + * @param buflen The length of the character array pointed-to by `buf` + * @return The pointer to the beginning of a null-terminated character array describing + * the status message. */ -static inline char* amongoc_status_strdup_message(amongoc_status s) { - return s.category->strdup_message(s.code); +inline const char* amongoc_message(amongoc_status st, char* buf, size_t buflen) mlib_noexcept { + const char* s = st.category->message(st.code, buf, buflen); + if (s) { + return s; + } + return "Message text unavailable"; } +#define amongoc_declmsg(VarName, Status) \ + _amongocDeclMsg(VarName, (Status), MLIB_PASTE(_amongoc_status_msg_mbuf, __LINE__)) +#define _amongocDeclMsg(VarName, Status, BufName) \ + char BufName[128]; \ + const char* VarName = amongoc_message((Status), BufName, sizeof(BufName)) + /** * @brief Obtain the reason code if-and-only-if the given status corresponds to a TLS error * diff --git a/src/amongoc/collection.cpp b/src/amongoc/collection.cpp index 6bfec10..cf8b802 100644 --- a/src/amongoc/collection.cpp +++ b/src/amongoc/collection.cpp @@ -23,18 +23,18 @@ using namespace amongoc; constexpr const amongoc_status_category_vtable amongoc_crud_category = { - .name = [] { return "amongoc.crud"; }, - .strdup_message = - [](int c) { - switch (static_cast<::amongoc_crud_errc>(c)) { - case ::amongoc_crud_okay: - return strdup("okay"); - case ::amongoc_crud_write_errors: - return strdup("The operation resulted in one or more write errors"); - default: - return strdup("Unknown error"); - } - }, + .name = [] { return "amongoc.crud"; }, + .message = [](int c, char* buf, size_t buflen) -> const char* { + switch (static_cast<::amongoc_crud_errc>(c)) { + case ::amongoc_crud_okay: + return "okay"; + case ::amongoc_crud_write_errors: + return "The operation resulted in one or more write errors"; + default: + std::snprintf(buf, buflen, "%s:%d", ::amongoc_crud_category.name(), c); + return buf; + } + }, .is_error = nullptr, .is_cancellation = nullptr, .is_timeout = nullptr, diff --git a/src/amongoc/status.c b/src/amongoc/status.c index 5dbf49b..93befd4 100644 --- a/src/amongoc/status.c +++ b/src/amongoc/status.c @@ -1,3 +1,4 @@ #include extern inline amongoc_status const* _amongocStatusGetOkayStatus(void) mlib_noexcept; +extern inline const char* amongoc_message(amongoc_status st, char* buf, size_t) mlib_noexcept; diff --git a/src/amongoc/status.cpp b/src/amongoc/status.cpp index 05287bf..0f601b4 100644 --- a/src/amongoc/status.cpp +++ b/src/amongoc/status.cpp @@ -22,13 +22,19 @@ class unknown_error_category : public std::error_category { struct io_category_cls : std::error_category { const char* name() const noexcept override { return "amongoc.io"; } std::string message(int ec) const noexcept override { + char buf[128]; + return message_noalloc(ec, buf, sizeof buf); + } + + const char* message_noalloc(int ec, char* buf, size_t buflen) const noexcept { switch (static_cast<::amongoc_io_errc>(ec)) { - case amongoc_errc_connection_closed: + case ::amongoc_errc_connection_closed: return "connection closed"; - case amongoc_errc_short_read: + case ::amongoc_errc_short_read: return "short read"; default: - return fmt::format("amongoc.io:{}", ec); + std::snprintf(buf, buflen, "amongoc.io:%d", ec); + return buf; } } } io_category_inst; @@ -36,12 +42,17 @@ struct io_category_cls : std::error_category { struct server_category_cls : std::error_category { const char* name() const noexcept override { return "amongoc.server"; } std::string message(int ec) const noexcept override { - std::string_view sv = _message_cstr(ec); - if (sv.empty()) { - // Unknown error code - return fmt::format("amongoc.server:{}", ec); + char buf[128]; + return this->message_noalloc(ec, buf, sizeof buf); + } + + const char* message_noalloc(int ec, char* buf, size_t buflen) const noexcept { + auto str = _message_cstr(ec); + if (str) { + return str; } - return std::string(sv); + std::snprintf(buf, buflen, "amongoc.server:%d", ec); + return buf; } const char* _message_cstr(int ec) const noexcept { @@ -726,20 +737,27 @@ struct server_category_cls : std::error_category { } } } - return ""; + return nullptr; } } server_category_inst; struct client_category_cls : std::error_category { const char* name() const noexcept override { return "amongoc.client"; } std::string message(int ec) const noexcept override { + char buf[128]; + return this->message_noalloc(ec, buf, sizeof buf); + } + + const char* message_noalloc(int ec, char* buf, size_t buflen) const noexcept { switch (static_cast<::amongoc_client_errc>(ec)) { case amongoc_client_errc_okay: return "no error"; case amongoc_client_errc_invalid_update_document: return "invalid document for an ‘update’ operation"; + default: + std::snprintf(buf, buflen, "amongoc.client:%d", ec); + return buf; } - return fmt::format("amongoc.client:{}", ec); } } client_category_inst; @@ -749,25 +767,47 @@ struct tls_category_cls : std::error_category { const char* msg = ::ERR_error_string(static_cast(ec), nullptr); return msg ? msg : fmt::format("amongoc.tls:{}", ec); } + + const char* message_noalloc(int ec, char* buf, size_t buflen) const noexcept { + const char* msg = ::ERR_error_string(static_cast(ec), nullptr); + if (msg) { + return msg; + } + std::snprintf(buf, buflen, "amongoc.tls:%d", ec); + return buf; + } } tls_category_inst; +const char* get_msg(const auto& cat, int c, char* buf, size_t buflen) { + std::string msg = cat.message(c); + std::snprintf(buf, buflen, "%*s", (int)msg.size(), msg.data()); + return buf; +} + +const char* get_msg(const auto& cat, int c, char* buf, size_t buflen) + requires requires(const char* msg) { msg = cat.message_noalloc(c, buf, buflen); } +{ + return cat.message_noalloc(c, buf, buflen); +} + // Inherit some status attributes from a C++ category template struct from_cxx_category { static const char* name() noexcept { return GetCategory().name(); } - static char* message(int c) { - std::string msg = GetCategory().message(c); - return ::strdup(msg.data()); + static const char* message(int c, char* buf, size_t buflen) noexcept { + return get_msg(GetCategory(), c, buf, buflen); } }; struct generic_category_attrs : from_cxx_category { - static bool is_timeout(int e) { return e == ETIMEDOUT; } - static bool is_cancellation(int e) { return e == ECANCELED; } + static bool is_timeout(int e) { return e == ETIMEDOUT; } + static bool is_cancellation(int e) { return e == ECANCELED; } + static const char* message(int c, char*, size_t) noexcept { return std::strerror(c); } }; struct system_category_attrs : from_cxx_category { - static bool is_timeout(int e) { return e == ETIMEDOUT; } - static bool is_cancellation(int e) { return e == ECANCELED; } + static bool is_timeout(int e) { return e == ETIMEDOUT; } + static bool is_cancellation(int e) { return e == ECANCELED; } + static const char* message(int c, char*, size_t) noexcept { return std::strerror(c); } }; struct netdb_category_attrs : from_cxx_category {}; @@ -802,7 +842,7 @@ constexpr auto get_is_cancellation = &T::is_cancellation; /* Definition of the category global instance */ \ constexpr ::amongoc_status_category_vtable MLIB_PASTE_3(amongoc_, Ident, _category) = { \ .name = &Attrs::name, \ - .strdup_message = &Attrs::message, \ + .message = &Attrs::message, \ .is_error = get_is_error, \ .is_cancellation = get_is_cancellation, \ .is_timeout = get_is_timeout, \ @@ -812,10 +852,8 @@ AMONGOC_STATUS_CATEGORY_X_LIST(); #undef DefCategory std::string status::message() const noexcept { - auto s = amongoc_status_strdup_message(*this); - std::string str(s); - free(s); - return str; + amongoc_declmsg(msg, *this); + return msg; } status status::from(const std::error_code& ec) noexcept { From 88932340be3a2487e54bce8524c596580c267c2f Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Wed, 22 Jan 2025 14:51:16 -0700 Subject: [PATCH 02/15] Add convenience overloads for amongoc_tie --- docs/how-to/communicate.example.c | 2 +- docs/how-to/communicate.rst | 5 +--- docs/how-to/looping.example.c | 2 +- docs/ref/async.rst | 6 ++++- include/amongoc/async.h | 41 +++++++++++++++++++++++++++++++ src/amongoc/async.c | 6 +++++ src/amongoc/async.cpp | 8 +++--- src/amongoc/coroutine.test.cpp | 2 +- src/amongoc/loop_fixture.test.hpp | 2 +- tests/sigcheck.test.h | 9 +++++++ 10 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 src/amongoc/async.c diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index c2f5590..8816a7a 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -89,7 +89,7 @@ int main(int argc, char const* const* argv) { after_connect_say_hello); amongoc_status fin_status = amongoc_okay; - amongoc_operation op = amongoc_tie(em, &fin_status, NULL, mlib_default_allocator); + amongoc_operation op = amongoc_tie(em, &fin_status); amongoc_start(&op); amongoc_default_loop_run(&loop); amongoc_operation_delete(op); diff --git a/docs/how-to/communicate.rst b/docs/how-to/communicate.rst index a82afb2..547ef24 100644 --- a/docs/how-to/communicate.rst +++ b/docs/how-to/communicate.rst @@ -208,10 +208,7 @@ the first continuation, we use `amongoc_tie` to convert the emitter to an :end-at: amongoc_tie This will allow us to see the final result status of the program in -``fin_status`` after the returned operation ``op`` completes. We pass ``NULL`` -for the `amongoc_tie::value` parameter, indicating that we do not care what the -final result value will be (in a successful case, this would just be the -`amongoc_nil` returned from ``after_hello``). +``fin_status`` after the returned operation ``op`` completes. Start the Operation, Run the Loop, and Clean Up diff --git a/docs/how-to/looping.example.c b/docs/how-to/looping.example.c index d3791b4..9b4bd7a 100644 --- a/docs/how-to/looping.example.c +++ b/docs/how-to/looping.example.c @@ -70,7 +70,7 @@ int main(int argc, char const* const* argv) { // Tie the final result for later, and start the program amongoc_status status; amongoc_box result; - amongoc_operation op = amongoc_tie(em, &status, &result, mlib_default_allocator); + amongoc_operation op = amongoc_tie(em, &status, &result); amongoc_start(&op); // Run the program within the event loop amongoc_default_loop_run(&loop); diff --git a/docs/ref/async.rst b/docs/ref/async.rst index b9f518c..b4fbc4d 100644 --- a/docs/ref/async.rst +++ b/docs/ref/async.rst @@ -253,7 +253,11 @@ Other :header: |this-header| -.. function:: amongoc_operation amongoc_tie(amongoc_emitter [[transfer, type(T)]] em, amongoc_status* [[storage]] st, amongoc_box* [[storage, type(T)]] value, mlib_allocator alloc) +.. function:: + amongoc_operation amongoc_tie(amongoc_emitter [[transfer]] em, amongoc_status* [[storage]] st) + amongoc_operation amongoc_tie(amongoc_emitter [[transfer, type(T)]] em, amongoc_box* [[storage, type(T)]] value) + amongoc_operation amongoc_tie(amongoc_emitter [[transfer, type(T)]] em, amongoc_status* [[storage]] st, amongoc_box* [[storage, type(T)]] value) + amongoc_operation amongoc_tie(amongoc_emitter [[transfer, type(T)]] em, amongoc_status* [[storage]] st, amongoc_box* [[storage, type(T)]] value, mlib_allocator alloc) Create an `amongoc_operation` object that captures the emitter's results in the given locations. diff --git a/include/amongoc/async.h b/include/amongoc/async.h index 8602186..c673213 100644 --- a/include/amongoc/async.h +++ b/include/amongoc/async.h @@ -412,6 +412,47 @@ amongoc_operation amongoc_tie(amongoc_emitter em, amongoc_box* value, mlib_allocator alloc) mlib_noexcept; +#define amongoc_tie(...) MLIB_ARGC_PICK(_amongoc_tie, __VA_ARGS__) +#define _amongoc_tie_argc_4(Em, StatusPtr, ValuePtr, Alloc) \ + amongoc_tie(Em, StatusPtr, ValuePtr, Alloc) +#define _amongoc_tie_argc_2(Em, StatusOrValuePtr) \ + mlib_generic(_amongoc_tie_cxx, \ + amongoc_tie, \ + (StatusOrValuePtr), \ + amongoc_status* : _amongoc_tie_status, \ + amongoc_box* : _amongoc_tie_value)((Em), (StatusOrValuePtr)) +#define _amongoc_tie_argc_3(Em, StatusPtr, ValuePtr) \ + amongoc_tie((Em), (StatusPtr), (ValuePtr), mlib_default_allocator) + +inline amongoc_operation _amongoc_tie_status(amongoc_emitter em, + amongoc_status* status) mlib_noexcept { + return amongoc_tie(em, status, NULL, mlib_default_allocator); +} +inline amongoc_operation _amongoc_tie_value(amongoc_emitter em, amongoc_box* value) mlib_noexcept { + return amongoc_tie(em, NULL, value, mlib_default_allocator); +} + +#if mlib_is_cxx() +extern "C++" { +inline amongoc_operation _amongoc_tie_cxx(amongoc_emitter em, amongoc_status* status) noexcept { + return amongoc_tie(em, status, nullptr, mlib_default_allocator); +} +inline amongoc_operation _amongoc_tie_cxx(amongoc_emitter em, amongoc_box* value) noexcept { + return amongoc_tie(em, nullptr, value, mlib_default_allocator); +} +inline amongoc_operation +_amongoc_tie_cxx(amongoc_emitter em, amongoc_status* status, amongoc_box* value) noexcept { + return amongoc_tie(em, status, value, mlib_default_allocator); +} +inline amongoc_operation _amongoc_tie_cxx(amongoc_emitter em, + amongoc_status* status, + amongoc_box* value, + mlib_allocator alloc) noexcept { + return amongoc_tie(em, status, value, alloc); +} +} +#endif // C++ + /** * @brief Create a "detached" operation from an emitter. This returns a simple operation * object that can be started. The final result from the emitter will simply be destroyed diff --git a/src/amongoc/async.c b/src/amongoc/async.c new file mode 100644 index 0000000..0c8889e --- /dev/null +++ b/src/amongoc/async.c @@ -0,0 +1,6 @@ +#include + +extern inline amongoc_operation _amongoc_tie_status(amongoc_emitter em, + amongoc_status* status) mlib_noexcept; +extern inline amongoc_operation _amongoc_tie_value(amongoc_emitter em, + amongoc_box* status) mlib_noexcept; diff --git a/src/amongoc/async.cpp b/src/amongoc/async.cpp index a537860..1034883 100644 --- a/src/amongoc/async.cpp +++ b/src/amongoc/async.cpp @@ -61,10 +61,10 @@ emitter amongoc_schedule(amongoc_loop* loop) { .release(); } -amongoc_operation amongoc_tie(amongoc_emitter em, - amongoc_status* status, - amongoc_box* value, - mlib_allocator alloc) mlib_noexcept { +amongoc_operation(amongoc_tie)(amongoc_emitter em, + amongoc_status* status, + amongoc_box* value, + mlib_allocator alloc) mlib_noexcept { // This function returns a different emitter depending on whether // the pointer values are null. If they are, we can returne an emitter // of a reduced size, reducing the need for memory allocations diff --git a/src/amongoc/coroutine.test.cpp b/src/amongoc/coroutine.test.cpp index dc42f40..bc89821 100644 --- a/src/amongoc/coroutine.test.cpp +++ b/src/amongoc/coroutine.test.cpp @@ -140,7 +140,7 @@ emitter throws_early(mlib::allocator<>) { TEST_CASE("Coroutine/Throw before suspend") { auto em = throws_early(::mlib_default_allocator); status st = ::amongoc_okay; - auto op = ::amongoc_tie(em, &st, nullptr, ::mlib_default_allocator).as_unique(); + auto op = ::amongoc_tie(em, &st).as_unique(); op.start(); CHECK(st.as_error_code() == std::errc::address_in_use); } diff --git a/src/amongoc/loop_fixture.test.hpp b/src/amongoc/loop_fixture.test.hpp index 471fa48..b4d6393 100644 --- a/src/amongoc/loop_fixture.test.hpp +++ b/src/amongoc/loop_fixture.test.hpp @@ -28,7 +28,7 @@ struct loop_fixture { emitter_result run_to_completion(amongoc_emitter em) noexcept { emitter_result ret; amongoc_box box; - unique_operation op = ::amongoc_tie(em, &ret.status, &box, ::mlib_default_allocator); + unique_operation op = ::amongoc_tie(em, &ret.status, &box); op.start(); loop.run(); op.reset(); diff --git a/tests/sigcheck.test.h b/tests/sigcheck.test.h index b27bb6e..6a1bd20 100644 --- a/tests/sigcheck.test.h +++ b/tests/sigcheck.test.h @@ -202,6 +202,15 @@ static inline void amongoc_test_all_signatures() { some_emitter = GLOBAL_SCOPE amongoc_just(amongoc_okay, amongoc_nil); some_emitter = GLOBAL_SCOPE amongoc_just(amongoc_nil, mlib_default_allocator); some_emitter = GLOBAL_SCOPE amongoc_just(); + + // tie() + amongoc_operation op; + amongoc_status status = GLOBAL_SCOPE amongoc_okay; + (void)op; + op = GLOBAL_SCOPE amongoc_tie(some_emitter, &status); + op = GLOBAL_SCOPE amongoc_tie(some_emitter, &some_userdata); + op = GLOBAL_SCOPE amongoc_tie(some_emitter, &status, &some_userdata); + op = GLOBAL_SCOPE amongoc_tie(some_emitter, &status, &some_userdata, mlib_default_allocator); } mlib_diagnostic_pop(); From f540423625424e5b1234c114ce9e967570bf9ae3 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Wed, 22 Jan 2025 14:51:31 -0700 Subject: [PATCH 03/15] Remove underscores on some macro'd functions --- include/amongoc/client.h | 4 ++-- include/amongoc/collection.h | 6 +++--- src/amongoc/client.cpp | 2 +- src/amongoc/collection.cpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/amongoc/client.h b/include/amongoc/client.h index 8f13534..b28937e 100644 --- a/include/amongoc/client.h +++ b/include/amongoc/client.h @@ -21,8 +21,8 @@ mlib_extern_c_begin(); * @param loop The event loop to be used * @param uri The connection URI string that specifies that peer and connection options */ -#define amongoc_client_new(Loop, URI) _amongoc_client_new((Loop), mlib_str_view_from((URI))) -amongoc_emitter _amongoc_client_new(amongoc_loop* loop, mlib_str_view uri) mlib_noexcept; +amongoc_emitter amongoc_client_new(amongoc_loop* loop, mlib_str_view uri) mlib_noexcept; +#define amongoc_client_new(Loop, URI) amongoc_client_new((Loop), mlib_str_view_from((URI))) /// Destroy an amongoc_client created with amongoc_client_new void amongoc_client_delete(amongoc_client* cl) mlib_noexcept; diff --git a/include/amongoc/collection.h b/include/amongoc/collection.h index f6faa47..4a41dab 100644 --- a/include/amongoc/collection.h +++ b/include/amongoc/collection.h @@ -31,10 +31,10 @@ mlib_extern_c_begin(); * @brief Obtain a CRUD handle to a collection within a database */ #define amongoc_collection_new(Client, DbName, CollName) \ - _amongoc_collection_new(Client, mlib_str_view_from(DbName), mlib_str_view_from(CollName)) -amongoc_collection* _amongoc_collection_new(amongoc_client* cl, + amongoc_collection_new(Client, mlib_str_view_from(DbName), mlib_str_view_from(CollName)) +amongoc_collection*(amongoc_collection_new)(amongoc_client* cl, mlib_str_view db_name, - mlib_str_view coll_name) mlib_noexcept; + mlib_str_view coll_name)mlib_noexcept; /** * @brief Delete a collection handle. Is a no-op for null handles. diff --git a/src/amongoc/client.cpp b/src/amongoc/client.cpp index adc2bae..f5456da 100644 --- a/src/amongoc/client.cpp +++ b/src/amongoc/client.cpp @@ -25,7 +25,7 @@ amongoc::wire::checking_pool_client amongoc_client::checking_wire_client() { return amongoc::wire::checking_client(amongoc::pool_client(_pool)); } -emitter _amongoc_client_new(amongoc_loop* loop, mlib_str_view uri_str) noexcept { +emitter(amongoc_client_new)(amongoc_loop* loop, mlib_str_view uri_str) noexcept { // Note: We copy the URI here before making the connect operation, because // we want to hold a copy of the URI string. auto uri = connection_uri::parse(uri_str, loop->get_allocator()); diff --git a/src/amongoc/collection.cpp b/src/amongoc/collection.cpp index cf8b802..0fdc08c 100644 --- a/src/amongoc/collection.cpp +++ b/src/amongoc/collection.cpp @@ -67,7 +67,7 @@ _parse_cursor(::amongoc_collection& coll, int batch_size, bson_view resp) { return mlib::unique(std::move(curs)); } -::amongoc_collection* _amongoc_collection_new(amongoc_client* cl, +::amongoc_collection*(amongoc_collection_new)(amongoc_client* cl, mlib_str_view db_name, mlib_str_view coll_name) noexcept try { auto ptr = cl->get_allocator().rebind().new_(*cl, From 1a7996b02c57585587edc288732f3ebb895cdfae Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 11:32:42 -0700 Subject: [PATCH 04/15] More convenient insert_one --- docs/ref/coll-ops/insert.rst | 3 ++- include/amongoc/collection.h | 6 ++++++ src/amongoc/collection.c | 7 ++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/ref/coll-ops/insert.rst b/docs/ref/coll-ops/insert.rst index 3e90446..49bfeac 100644 --- a/docs/ref/coll-ops/insert.rst +++ b/docs/ref/coll-ops/insert.rst @@ -4,7 +4,8 @@ Insert .. function:: amongoc_emitter [[type(amongoc_write_result)]] amongoc_insert_ex(amongoc_collection* coll, const bson_view* documents, size_t n_docs, amongoc_insert_params const* [[nullable]] params) - amongoc_emitter [[type(amongoc_write_result)]] amongoc_insert_one(amongoc_collection* coll, bson_view doc, amongoc_insert_params const* [[nullable]] params) + amongoc_emitter [[type(amongoc_write_result)]] amongoc_insert_one(amongoc_collection* coll, __bson_viewable doc) + amongoc_emitter [[type(amongoc_write_result)]] amongoc_insert_one(amongoc_collection* coll, __bson_viewable doc, amongoc_insert_params const* [[nullable]] params) Insert data into the collection. Resolves with an `amongoc_write_result`. diff --git a/include/amongoc/collection.h b/include/amongoc/collection.h index 4a41dab..3d0f2fc 100644 --- a/include/amongoc/collection.h +++ b/include/amongoc/collection.h @@ -220,6 +220,12 @@ inline amongoc_emitter amongoc_insert_one(amongoc_collection* coll, return amongoc_insert_ex(coll, &doc, 1, params); } +#define amongoc_insert_one(...) MLIB_ARGC_PICK(_amongoc_insert_one, __VA_ARGS__) +#define _amongoc_insert_one_argc_2(Coll, Doc) \ + amongoc_insert_one((Coll), bson_view_from((Doc)), NULL) +#define _amongoc_insert_one_argc_3(Coll, Doc, Params) \ + amongoc_insert_one((Coll), bson_view_from((Doc)), (Params)) + // d88888b d888888b d8b db d8888b. // 88' `88' 888o 88 88 `8D // 88ooo 88 88V8o 88 88 88 diff --git a/src/amongoc/collection.c b/src/amongoc/collection.c index a23e25c..f587f7a 100644 --- a/src/amongoc/collection.c +++ b/src/amongoc/collection.c @@ -9,9 +9,10 @@ amongoc_delete_many(amongoc_collection* coll, bson_view filter, struct amongoc_delete_params const* params) mlib_noexcept; -extern inline amongoc_emitter amongoc_insert_one(amongoc_collection* coll, - bson_view doc, - amongoc_insert_params const* params) mlib_noexcept; +extern inline amongoc_emitter(amongoc_insert_one)(amongoc_collection* coll, + bson_view doc, + amongoc_insert_params const* params) + mlib_noexcept; extern inline amongoc_emitter amongoc_find_one_and_delete(amongoc_collection* coll, From cc382c984392acf18119307918a624228bd889bb Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 11:33:39 -0700 Subject: [PATCH 05/15] Mark default loop init with nodiscard --- docs/how-to/communicate.example.c | 16 ++++++++++------ docs/how-to/communicate.rst | 4 ++-- docs/learn/connect.example.c | 9 +++++++-- docs/learn/connect.rst | 2 +- include/amongoc/default_loop.h | 10 ++++++---- include/amongoc/status.h | 7 +++++++ src/amongoc/async.test.cpp | 4 ++-- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index 8816a7a..cd6542b 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -72,8 +72,13 @@ int main(int argc, char const* const* argv) { } const char* const uri = argv[1]; - amongoc_loop loop; - amongoc_default_loop_init(&loop); + amongoc_loop loop; + amongoc_status status = amongoc_default_loop_init(&loop); + if (amongoc_is_error(status)) { + amongoc_declmsg(msg, status); + fprintf(stderr, "Error setting up the event loop: %s\n", msg); + return 2; + } struct app_state state = {0}; @@ -88,8 +93,7 @@ int main(int argc, char const* const* argv) { amongoc_box_pointer(&state), after_connect_say_hello); - amongoc_status fin_status = amongoc_okay; - amongoc_operation op = amongoc_tie(em, &fin_status); + amongoc_operation op = amongoc_tie(em, &status); amongoc_start(&op); amongoc_default_loop_run(&loop); amongoc_operation_delete(op); @@ -98,8 +102,8 @@ int main(int argc, char const* const* argv) { amongoc_client_delete(state.client); amongoc_default_loop_destroy(&loop); - if (amongoc_is_error(fin_status)) { - amongoc_declmsg(msg, fin_status); + if (amongoc_is_error(status)) { + amongoc_declmsg(msg, status); fprintf(stderr, "An error occurred: %s\n", msg); return 2; } else { diff --git a/docs/how-to/communicate.rst b/docs/how-to/communicate.rst index 547ef24..a773738 100644 --- a/docs/how-to/communicate.rst +++ b/docs/how-to/communicate.rst @@ -46,7 +46,7 @@ The first "interesting" code will declare and initialize the default event loop: .. literalinclude:: communicate.example.c :lineno-match: :start-at: loop; - :end-at: ); + :end-at: } .. seealso:: `amongoc_loop` and `amongoc_default_loop_init` @@ -204,7 +204,7 @@ the first continuation, we use `amongoc_tie` to convert the emitter to an .. literalinclude:: communicate.example.c :lineno-match: - :start-at: fin_status + :start-at: amongoc_tie :end-at: amongoc_tie This will allow us to see the final result status of the program in diff --git a/docs/learn/connect.example.c b/docs/learn/connect.example.c index 458247c..e8d7c07 100644 --- a/docs/learn/connect.example.c +++ b/docs/learn/connect.example.c @@ -7,8 +7,13 @@ amongoc_box on_connect(amongoc_box userdata, amongoc_status* status, amongoc_box result); int main(void) { - amongoc_loop loop; - amongoc_default_loop_init(&loop); + amongoc_loop loop; + amongoc_status status = amongoc_default_loop_init(&loop); + if (amongoc_is_error(status)) { + amongoc_declmsg(msg, status); + fprintf(stderr, "Failed to prepare the event loop: %s\n", msg); + return 2; + } // Initiate a connection amongoc_emitter em = amongoc_client_new(&loop, "mongodb://localhost:27017"); diff --git a/docs/learn/connect.rst b/docs/learn/connect.rst index 2ab374d..c4dae5c 100644 --- a/docs/learn/connect.rst +++ b/docs/learn/connect.rst @@ -29,7 +29,7 @@ programs: .. literalinclude:: connect.example.c :caption: Create a default event loop :start-at: int main - :end-at: default_loop_init + :end-at: } :lineno-match: :dedent: diff --git a/include/amongoc/default_loop.h b/include/amongoc/default_loop.h index a9ac02a..cce95b5 100644 --- a/include/amongoc/default_loop.h +++ b/include/amongoc/default_loop.h @@ -8,9 +8,11 @@ mlib_extern_c_begin(); -extern amongoc_status amongoc_default_loop_init_with_allocator(amongoc_loop* loop, - mlib_allocator) mlib_noexcept; -static inline amongoc_status amongoc_default_loop_init(amongoc_loop* loop) mlib_noexcept { +mlib_nodiscard("This function may fail to allocate resources") extern amongoc_status + amongoc_default_loop_init_with_allocator(amongoc_loop* loop, mlib_allocator) mlib_noexcept; + +mlib_nodiscard("This function may fail to allocate resources") static inline amongoc_status + amongoc_default_loop_init(amongoc_loop* loop) mlib_noexcept { return amongoc_default_loop_init_with_allocator(loop, mlib_default_allocator); } @@ -24,7 +26,7 @@ namespace amongoc { struct default_event_loop { public: - default_event_loop() { ::amongoc_default_loop_init(&_loop); } + default_event_loop() { ::amongoc_default_loop_init(&_loop).throw_for_error(); } ~default_event_loop() { ::amongoc_default_loop_destroy(&_loop); } default_event_loop(default_event_loop&&) = delete; diff --git a/include/amongoc/status.h b/include/amongoc/status.h index ef5a07d..d9f58a4 100644 --- a/include/amongoc/status.h +++ b/include/amongoc/status.h @@ -797,6 +797,8 @@ struct amongoc_status { constexpr bool operator!=(amongoc_status const& other) const noexcept { return not(*this == other); } + + inline void throw_for_error() const; #endif }; @@ -895,4 +897,9 @@ class exception : std::runtime_error { bool amongoc_status::is_error() const noexcept { return amongoc_is_error(*this); } +void amongoc_status::throw_for_error() const { + if (this->is_error()) { + throw amongoc::exception(*this); + } +} #endif diff --git a/src/amongoc/async.test.cpp b/src/amongoc/async.test.cpp index 288fa4c..e8fa095 100644 --- a/src/amongoc/async.test.cpp +++ b/src/amongoc/async.test.cpp @@ -45,7 +45,7 @@ TEST_CASE("Async/Transform with the C API") { TEST_CASE("Async/Timeout") { amongoc_loop loop; - amongoc_default_loop_init(&loop); + amongoc_default_loop_init(&loop).throw_for_error(); // One minute delay (too slow) auto dur = timespec{}; dur.tv_sec = 60; @@ -93,7 +93,7 @@ emitter waits(amongoc_loop& loop) { TEST_CASE("Async/let") { amongoc_loop loop; - amongoc_default_loop_init(&loop); + amongoc_default_loop_init(&loop).throw_for_error(); auto em = amongoc_let(waits(loop), amongoc_async_forward_errors, mlib_default_allocator, From b8fb2254fb069c3ef93491ff5be9d0c8019c1a10 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 12:52:26 -0700 Subject: [PATCH 06/15] More status handling shorthands --- .clang-format | 1 + docs/conf.py | 2 ++ docs/how-to/communicate.example.c | 6 ++--- docs/how-to/communicate.rst | 2 +- docs/how-to/looping.example.c | 17 ++++++++------ docs/how-to/looping.rst | 10 ++++---- docs/learn/connect.example.c | 6 ++--- docs/ref/status.rst | 38 +++++++++++++++++++++++++++++-- include/amongoc/status.h | 26 ++++++++++++++++++++- src/amongoc/status.test.cpp | 15 ++++++++++++ 10 files changed, 99 insertions(+), 24 deletions(-) diff --git a/.clang-format b/.clang-format index 42be65c..6b89373 100644 --- a/.clang-format +++ b/.clang-format @@ -82,6 +82,7 @@ IncludeCategories: InsertNewlineAtEOF: true IfMacros: - mlib_math_catch + - amongoc_if_error AllowBreakBeforeNoexceptSpecifier: Always --- # For some reason, Clang sees some files as Objective-C. Add this section just to appease it. diff --git a/docs/conf.py b/docs/conf.py index b455450..dafca60 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -415,6 +415,8 @@ class CustomCppLexer(CppLexer): (r"bson_type_\w+\b", token.Name.Constant), # Other macro (r"bson_foreach\w*\b", token.Keyword), + (r"amongoc_declmsg\b", token.Keyword), + (r"amongoc_if_error\b", token.Keyword), inherit, ], } diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index cd6542b..e35dbef 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -74,8 +74,7 @@ int main(int argc, char const* const* argv) { amongoc_loop loop; amongoc_status status = amongoc_default_loop_init(&loop); - if (amongoc_is_error(status)) { - amongoc_declmsg(msg, status); + amongoc_if_error (status, msg) { fprintf(stderr, "Error setting up the event loop: %s\n", msg); return 2; } @@ -102,8 +101,7 @@ int main(int argc, char const* const* argv) { amongoc_client_delete(state.client); amongoc_default_loop_destroy(&loop); - if (amongoc_is_error(status)) { - amongoc_declmsg(msg, status); + amongoc_if_error (status, msg) { fprintf(stderr, "An error occurred: %s\n", msg); return 2; } else { diff --git a/docs/how-to/communicate.rst b/docs/how-to/communicate.rst index a773738..9094fa1 100644 --- a/docs/how-to/communicate.rst +++ b/docs/how-to/communicate.rst @@ -240,7 +240,7 @@ Print the Final Result .. literalinclude:: communicate.example.c :lineno-match: - :start-at: is_error + :start-at: if_error :end-before: end. Finally, we inspect the `amongoc_status` that was produced by our operation and diff --git a/docs/how-to/looping.example.c b/docs/how-to/looping.example.c index 9b4bd7a..6ae184d 100644 --- a/docs/how-to/looping.example.c +++ b/docs/how-to/looping.example.c @@ -20,7 +20,9 @@ typedef struct { * * @param state_ptr Pointer to the `state` for the program */ -amongoc_emitter loop_step(amongoc_box state_ptr, amongoc_status prev_status, amongoc_box prev_res) { +amongoc_emitter loop_step(amongoc_box state_ptr, // + amongoc_status prev_status, + amongoc_box prev_res) { (void)prev_res; (void)prev_status; // Print our status @@ -60,7 +62,10 @@ int main(int argc, char const* const* argv) { // Create a default loop amongoc_loop loop; - amongoc_default_loop_init(&loop); + amongoc_if_error (amongoc_default_loop_init(&loop), msg) { + fprintf(stderr, "Error initializing event loop: %s\n", msg); + return 1; + } // Seed the initial sum state app_state = {.countdown = delay, .loop = &loop, .a = 0, .b = 1}; @@ -77,14 +82,12 @@ int main(int argc, char const* const* argv) { amongoc_operation_delete(op); amongoc_default_loop_destroy(&loop); - if (amongoc_is_error(status)) { - amongoc_declmsg(msg, status); + amongoc_if_error (status, msg) { fprintf(stderr, "error: %s\n", msg); amongoc_box_destroy(result); return 2; - } else { - // Get the value returned with `amongoc_just` in `loop_step` - printf("Got final value: %lu\n", amongoc_box_cast(uint64_t, result)); } + // Get the value returned with `amongoc_just` in `loop_step` + printf("Got final value: %lu\n", amongoc_box_cast(uint64_t, result)); return 0; } diff --git a/docs/how-to/looping.rst b/docs/how-to/looping.rst index ae7cf0b..f6fe503 100644 --- a/docs/how-to/looping.rst +++ b/docs/how-to/looping.rst @@ -196,14 +196,14 @@ program. We check for errors, either printing the error message or printing the final result: .. literalinclude:: looping.example.c - :start-at: is_error + :start-at: if_error (status, msg) :end-at: return 0; :lineno-match: -We use `amongoc_is_error` to test the final status for an error condition. If it -is an error, we get and print the error message to stderr, and we must destroy -the final result box because it may contain an unspecified value related to the -error, but we don't want to do anything with it. +We use :c:macro:`amongoc_if_error` to test the final status for an error +condition. If it is an error, we get and print the error message to stderr, and +we must destroy the final result box because it may contain an unspecified value +related to the error, but we don't want to do anything with it. In the success case, we extract the value returned in `amongoc_just` as a ``uint64_t`` and print it to stdout. Note that because the box returned by diff --git a/docs/learn/connect.example.c b/docs/learn/connect.example.c index e8d7c07..7f3c1d5 100644 --- a/docs/learn/connect.example.c +++ b/docs/learn/connect.example.c @@ -9,8 +9,7 @@ amongoc_box on_connect(amongoc_box userdata, amongoc_status* status, amongoc_box int main(void) { amongoc_loop loop; amongoc_status status = amongoc_default_loop_init(&loop); - if (amongoc_is_error(status)) { - amongoc_declmsg(msg, status); + amongoc_if_error (status, msg) { fprintf(stderr, "Failed to prepare the event loop: %s\n", msg); return 2; } @@ -32,8 +31,7 @@ amongoc_box on_connect(amongoc_box userdata, amongoc_status* status, amongoc_box // We don't use the userdata (void)userdata; // Check for an error - if (amongoc_is_error(*status)) { - amongoc_declmsg(msg, *status); + amongoc_if_error (*status, msg) { fprintf(stderr, "Error while connecting to server: %s\n", msg); } else { printf("Successfully connected!\n"); diff --git a/docs/ref/status.rst b/docs/ref/status.rst index 9ce50b8..d772f0b 100644 --- a/docs/ref/status.rst +++ b/docs/ref/status.rst @@ -83,6 +83,8 @@ Functions & Macros The defintion of what constitutes an error depends on the `amongoc_status::category`. + .. seealso:: :c:macro:`amongoc_if_error` + .. function:: bool amongoc_is_cancellation(amongoc_status st) @@ -130,8 +132,12 @@ Functions & Macros This function does not dynamically allocate any memory. - .. seealso:: :c:macro:`amongoc_declmsg` for concisely obtaining the message - from a status object. + .. seealso:: + + - :c:macro:`amongoc_declmsg` for concisely obtaining the message from a + status object. + - :c:macro:`amongoc_if_error` to check for an error and extract the message + in a single line. .. c:macro:: amongoc_declmsg(MsgVar, Status) @@ -151,6 +157,34 @@ Functions & Macros const char* MsgVar = amongoc_message(Status, __buffer, sizeof __buffer) +.. c:macro:: + amongoc_if_error(Status, MsgVar, StatusVar) + + Create a branch on whether the given status represents an error. This macro + supports being called with two arguments, or with three:: + + amongoc_if_error (status, msg_varname) { + fprintf(stderr, "Error message: %s\n", msg_varname) + } + + :: + + amongoc_if_error (status, msg_varname, status_varname) { + fprintf(stderr, "Error code %d has message: %s\n", status_varname.code, msg_varname); + } + + :param Status: The first argument must be an expression of type `amongoc_status`. This is + the status to be inspected. + :param MsgVar: This argument should be a plain identifier, which will be declared within + the scope of the statement as the :term:`C string` for the status. + :param StatusVar: If provided, a variable of type `amongoc_status` will be declared within + the statment scope that captures the value of the ``Status`` argument. + + .. hint:: + + If you are using ``clang-format``, add ``amongoc_if_error`` to the + ``IfMacros`` for your ``clang-format`` configuration. + .. var:: const amongoc_status amongoc_okay A generic status with a code zero. This represents a generic non-error status. diff --git a/include/amongoc/status.h b/include/amongoc/status.h index d9f58a4..3996070 100644 --- a/include/amongoc/status.h +++ b/include/amongoc/status.h @@ -871,8 +871,32 @@ mlib_extern_c_end(); #define amongoc_okay mlib_parenthesized_expression(*_amongocStatusGetOkayStatus()) -#if mlib_is_cxx() +/** + * @brief Branch on whether a status code is an error, and get the string message + * out of it in a single statement. + * + * @param Status An expression of type `amongoc_status` + * @param MsgVar An identifier, which will be declared as a C string for the status' message + */ +#define amongoc_if_error(...) MLIB_ARGC_PICK(_amongoc_if_error, __VA_ARGS__) +#define _amongoc_if_error_argc_2(Status, MsgVar) \ + _amongoc_if_error_argc_3((Status), MsgVar, MLIB_PASTE(_amongoc_status_tmp_lno_, __LINE__)) +#define _amongoc_if_error_argc_3(Status, MsgVar, StatusVar) \ + _amongocIfErrorBlock((Status), \ + MsgVar, \ + StatusVar, \ + MLIB_PASTE(_amongoc_oncevar_lno_, __LINE__), \ + MLIB_PASTE(_amongoc_msgbuf_lno_, __LINE__)) +// clang-format off +#define _amongocIfErrorBlock(Status, MsgVar, StatusVar, OnceVar, MsgBuf) \ + for (int OnceVar = 1; OnceVar; OnceVar = 0) \ + for (amongoc_status const StatusVar = (Status); OnceVar; OnceVar = 0) \ + if (amongoc_is_error(StatusVar)) \ + for (char MsgBuf[128]; OnceVar; OnceVar = 0) \ + for (const char* MsgVar = amongoc_message(StatusVar, MsgBuf, sizeof MsgBuf); OnceVar; OnceVar = 0) +// clang-format on +#if mlib_is_cxx() namespace amongoc { using status = ::amongoc_status; diff --git a/src/amongoc/status.test.cpp b/src/amongoc/status.test.cpp index 7cb3d3f..204269b 100644 --- a/src/amongoc/status.test.cpp +++ b/src/amongoc/status.test.cpp @@ -8,12 +8,27 @@ TEST_CASE("Status/Okay") { amongoc_status st = amongoc_okay; CHECK(st.code == 0); CHECK(st.category == &amongoc_generic_category); + + bool took_else = false; + amongoc_if_error (st, _) { + FAIL_CHECK("Did not expect an error"); + } else { + took_else = true; + } + CHECK(took_else); } TEST_CASE("Status/From an Error") { auto st = amongoc_status::from(std::make_error_code(std::errc::io_error)); CHECK(st.category == &amongoc_generic_category); CHECK(st.code == EIO); + bool took_err = false; + amongoc_if_error (st, _) { + took_err = true; + } else { + FAIL_CHECK("Did not take the error branch"); + } + CHECK(took_err); } TEST_CASE("Status/As an Error") { From 25d0d3d4dc13a751d757c1b26426dfff83da9881 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 12:59:05 -0700 Subject: [PATCH 07/15] Tutorial: Writing data --- docs/learn/index.rst | 1 + docs/learn/write.example.c | 83 ++++++++++++++ docs/learn/write.rst | 221 +++++++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 docs/learn/write.example.c create mode 100644 docs/learn/write.rst diff --git a/docs/learn/index.rst b/docs/learn/index.rst index 8b27d91..0f5e3d0 100644 --- a/docs/learn/index.rst +++ b/docs/learn/index.rst @@ -9,3 +9,4 @@ Tutorials bson/index box connect + write diff --git a/docs/learn/write.example.c b/docs/learn/write.example.c new file mode 100644 index 0000000..06a1812 --- /dev/null +++ b/docs/learn/write.example.c @@ -0,0 +1,83 @@ +#include + +#include + +// Application state +typedef struct { + // The client object + amongoc_client* client; + // The collection object which we will write into + amongoc_collection* coll; +} app_state; + +static amongoc_emitter on_connect(amongoc_box state, amongoc_status status, amongoc_box client); + +int main() { + amongoc_loop loop; + amongoc_status status = amongoc_default_loop_init(&loop); + amongoc_if_error (status, msg) { + fprintf(stderr, "Failed to initialize event loop: %s\n", msg); + return 1; + } + // Connect + amongoc_emitter em = amongoc_client_new(&loop, "mongodb://localhost:27017"); + + // Application state object + app_state state = {0}; + + // Set the continuation for the connection: + em = amongoc_let(em, // [1] + amongoc_async_forward_errors, // [2] + amongoc_box_pointer(&state), // [3] + on_connect); // [4] + + amongoc_operation op = amongoc_tie(em, &status); + amongoc_start(&op); + amongoc_default_loop_run(&loop); + + // Clean up: + amongoc_operation_delete(op); + amongoc_collection_delete(state.coll); + amongoc_client_delete(state.client); + amongoc_default_loop_destroy(&loop); + + // Print the final result + amongoc_if_error (status, msg) { + fprintf(stderr, "An error occurred: %s\n", msg); + return 1; + } + printf("okay\n"); + return 0; +} + +// Continuation after connection completes +static amongoc_emitter on_connect(amongoc_box state_, amongoc_status status, amongoc_box client) { + // We don't use the status here + (void)status; + // Store the client from the connect operation + app_state* const state = amongoc_box_cast(app_state*, state_); + amongoc_box_take(state->client, client); + + // Create a new collection handle + state->coll = amongoc_collection_new(state->client, "write-test-db", "main"); + if (!state->coll) { + return amongoc_alloc_failure(); + } + + // Create a document to be inserted + // Data: { "foo": "bar", "answer": 42 } + bson_doc doc = bson_new(); + if (!bson_data(doc)) { + return amongoc_alloc_failure(); + } + bson_mut mut = bson_mutate(&doc); + bson_insert(&mut, "foo", "bar"); + bson_insert(&mut, "answer", 42); + // Insert the single document: + amongoc_emitter insert_em = amongoc_insert_one(state->coll, doc); + // Delete our copy of the doc + bson_delete(doc); + // Tell the runtime to continue into the next operation: + return insert_em; +} +// end:on_connect diff --git a/docs/learn/write.rst b/docs/learn/write.rst new file mode 100644 index 0000000..3d1041e --- /dev/null +++ b/docs/learn/write.rst @@ -0,0 +1,221 @@ +############ +Writing Data +############ + +This tutorial will cover the basics of writing data into a MongoDB server +asynchronously. This page will assume that you have read the content of +the :doc:`connect` tutorial. It will assume that you have a program that +includes the |amongoc| headers and know how to connect to a server. + + +Declaring Application State Type +################################ + +This program will require that certain objects outlive the sub-operations, so we +will need to persist them outside the event loop and pass them to our +continuation. We declare a simple aggregate type to hold these objects: + +.. literalinclude:: write.example.c + :caption: Application State Struct + :start-at: // Application state + :end-at: app_state; + :lineno-match: + + +Starting with a Connection +########################## + +We'll start with the basics of setting up an event loop and creating a +connection :term:`emitter`: + +.. literalinclude:: write.example.c + :caption: Setting up an event loop + :start-at: int main + :end-at: amongoc_client_new + :lineno-match: + + +Declare the Application State Object +#################################### + +Declare an instance of ``app_state``, which will be passed through the program +by address. We zero-initialize it so that the pointer members are null, making +deletion a no-op in case they are never initialized later. + +.. literalinclude:: write.example.c + :caption: Application state object + :start-at: // Application state object + :end-at: ; + :lineno-match: + + +Create the First Continuation +############################# + +We have the pending connection object and the application state, so now we can +set the first continuation: + +.. literalinclude:: write.example.c + :caption: The first continuation + :start-at: Set the continuation + :end-at: on_connect); + :lineno-match: + +The arguments are as follows: + +1. Passing the emitter we got from `amongoc_client_new`, and replacing it with + the new operation emitter returned by `amongoc_let`. +2. `amongoc_async_forward_errors` is a behavior control flag for `amongoc_let` + that tells the operation to skip our continuation if the input operation + generates an error. +3. We pass the address of the ``state`` object by wrapping it with + `amongoc_box_pointer`, passing it as the ``userdata`` parameter of + `amongoc_let`. +4. ``on_connect`` is the name of the function that will handle the continuation. + + +Define the Continuation +####################### + +Now we can look at the definition of ``on_connect``: + +.. literalinclude:: write.example.c + :caption: Continuation signature + :start-at: // Continuation after connection + :end-at: (void)status; + :lineno-match: + +The ``state_`` parameter is the userdata box that was given to `amongoc_let` in +the previous section. The ``client`` parameter is a box that contains the +`amongoc_client` pointer from the connection operation. The ``status`` parameter +here is the status of the connection operation, but we don't care about this +here since we used `amongoc_async_forward_errors` to ask `amongoc_let` to skip +this function if the status would otherwise indicate an error (i.e. +``on_connect`` will only be called if the connection actually succeeds). + + +Update the Application State +**************************** + +``on_connect`` is passed the pointer to the application state as well as a box +containing a pointer to the `amongoc_client`. We can extract the box value and +store the client pointer within our application state struct: + +.. literalinclude:: write.example.c + :caption: Update the Application State + :start-at: // Store the client + :end-at: amongoc_box_take + :lineno-match: + +We store it in the application state object so that we can later delete the +client handle when the program completes. + + +Create a Collection Handle +************************** + +Now that we have a valid client, we can create a handle to a collection on +the server: + +.. literalinclude:: write.example.c + :caption: Set up the Collection + :start-at: Create a new collection handle + :end-at: } + :lineno-match: + +`amongoc_collection_new` does not actually communicate with the server: It only +creates a client-side handle that can be used to perform operations related to +that collection. The server-side collection will be created automatically when +we begin writing data into it. + +We store the returned collection handle in the state struct so that we can later +delete it. + + +Create Some Data to Insert +************************** + +To insert data, we first need data to be inserted. We can create that with the +BSON APIs: + +.. literalinclude:: write.example.c + :caption: Create a Document + :start-at: // Create a document + :end-before: // Insert the single + +.. seealso:: :doc:`bson/index` for tutorials on the BSON APIs. + + +Create the Insert Operation +*************************** + +Inserting a single document can be done with `amongoc_insert_one`: + +.. literalinclude:: write.example.c + :caption: Insert the Data + :start-at: // Insert the single + :end-at: return insert_em + :lineno-match: + +`amongoc_insert_one` will take a copy of the data, so we can delete it +immediately. We then return the resulting insert emitter from ``on_connect`` to +tell `amongoc_let` to continue the composed operation. + + +The Full Continuation +********************* + +Here is the full ``on_connect`` function: + +.. literalinclude:: write.example.c + :caption: The ``on_connect`` function + :start-at: // Continuation after + :end-before: end:on_connect + :lineno-match: + + +Tie, Start, and Run the Operation +################################# + +Going back to ``main``, we can now tie the operation, start it, and run the +event loop: + +.. literalinclude:: write.example.c + :caption: Run the Program + :start-at: amongoc_tie + :end-at: loop_destroy + :lineno-match: + +`amongoc_tie` tells the emitter to store its final status in the given pointer +destination, and returns an operation that can be initiated with +`amongoc_start`. We then give control to the event loop with +`amongoc_default_loop_run`. After this returns, the operation object is +finished, so we delete it with `amongoc_operation_delete`. + +We are also finished with the collection handle and the client, so we delete +those here as well. Those struct members will have been filled in by +``on_connect``. + + +Error Checking +############## + +The ``status`` object will have been filled during `amongoc_default_loop_run` +due to the call to `amongoc_tie`. We can check it for errors and print them out +now: + +.. literalinclude:: write.example.c + :caption: Check the Final Status + :start-at: // Print the final + :end-at: return 0; + :lineno-match: + + +The Full Program +################ + +Here is the full sample program, in its entirety: + +.. literalinclude:: write.example.c + :caption: ``write.example.c`` + :linenos: From be8ae4c85ae965d9f461e2703031b1b9c62b3b01 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 12:59:35 -0700 Subject: [PATCH 08/15] How-to: Status handling --- docs/how-to/index.rst | 1 + docs/how-to/status-handling.rst | 114 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 docs/how-to/status-handling.rst diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 6ea31bb..f0a9094 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -6,5 +6,6 @@ How-to Guides :caption: Guides :maxdepth: 2 + status-handling communicate looping diff --git a/docs/how-to/status-handling.rst b/docs/how-to/status-handling.rst new file mode 100644 index 0000000..7b9df3b --- /dev/null +++ b/docs/how-to/status-handling.rst @@ -0,0 +1,114 @@ +############################## +How to Handle `amongoc_status` +############################## + +The `amongoc_status` type is an extensible status-reporting type, allowing +different subsystems to share integral error code values and understand each +other's error conditions without universal coordination on the meaning of opaque +integral values. A status object has two salient properties: A +`code `, and a `category `. +Generally, user code should not inspect the ``code`` without also respecting the +``category``. + + +Checking for Errors +################### + +To test whether a particular opaque status represents a failure condition, **do +not** be tempted to inspect the code value directly. Instead, use +`amongoc_is_error`:: + + amongoc_status some_status = some_function(); + + // BAD: + if (some_status.code) { + // ... + } + + // GOOD: + if (amongoc_is_error(some_status)) { + // ... + } + +While it is conventional that an integer value zero represents a successful +status, it is possible that the status' category may have more than one success +code value, and they may be non-zero. `amongoc_is_error` consults the status +category to determine whether the status code indicates an error. + + +Obtaining an Error Message +########################## + +The status category also knows how to convert an integer value into a +human-readable status message. The `amongoc_message` function will obtain a +:term:`C string` that represents the message for a status:: + + const char* msg = amongoc_message(some_status, NULL, 0); + printf("Status message: %s\n", msg); + +Supporting Dynamic Messages +*************************** + +A status category may need to dynamically generate a message string based on the +integer value. In order to do this, it needs space to write the message string. +This is the purpose of the second and third parameters to `amongoc_message`:: + + char msgbuf[128]; + const char* msg = amongoc_message(some_status, msgbuf, sizeof msgbuf); + printf("Status message: %s\n", msg); + +The second parameter must be a pointer to a modifiable array of |char|, and the +third parameter must be the number of characters available to be written at that +pointer. *If* `amongoc_message` needs to dynamically generate a message string, +it will use the provided buffer and return a pointer to that same buffer. +`amongoc_message` *might* or *might not* use the message buffer to generate the +string, so you **should not** expect the character array to be modified by +`amongoc_message`. Always use the returned :term:`C string` as the status +message. + +.. note:: + + If you do not provide a writable buffer for the message, `amongoc_message` may + return a fallback string that loses information about the status, so it is + best to always provide the writable buffer. + + +A Shorthand +*********** + +Because declaring a writable buffer and calling `amongoc_message` is so common, +a shorthand macro :c:macro:`amongoc_declmsg` is defined that can do this +boilerplate in a single line:: + + amongoc_declmsg(msg_var, some_status); + printf("Status msesage: %s\n", msg_var); + +Internally, this will declare a small writable buffer and call `amongoc_message` +for us. The first parameter is the name of a variable to be declared, and the +second parameter is the `amongoc_status` to be inspected. + + +Easy Error Handling +################### + +You may find yourself writing code like this, repeatedly:: + + amognoc_status status = some_function(); + if (amongoc_is_error(status)) { + amongoc_declmsg(msg, status); + fprintf(stderr, "The function failed: %s\n", msg); + return 42; + } else { + fprintf(stderr, "The function succeeded\n"); + return 0; + } + +A macro, :c:macro:`amongoc_if_error`, can be used to do this more concisely:: + + amongoc_if_error (some_function(), msg) { + fprintf(stderr, "The function failed: %s\n", msg); + return 42; + } else { + fprintf(stderr, "The function succeeded\n"); + return 0; + } From efd22da168eee734d7d2facaff383915ab23c261 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 16:45:11 -0700 Subject: [PATCH 09/15] Output stream abstraction --- include/mlib/stream.h | 67 +++++++++++++++++++++++++++++++++++++++++++ src/mlib/stream.c | 4 +++ src/mlib/stream.cpp | 26 +++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 include/mlib/stream.h create mode 100644 src/mlib/stream.c create mode 100644 src/mlib/stream.cpp diff --git a/include/mlib/stream.h b/include/mlib/stream.h new file mode 100644 index 0000000..a4135b5 --- /dev/null +++ b/include/mlib/stream.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include + +/** + * @brief An abstract writable stream interface. + */ +typedef struct mlib_ostream { + /// Arbitrary userdata pointer associated with the stream + void* userdata; + /// The write callback that handles writing data to the stream. + size_t (*write)(void* userdata, const char* bufs, size_t buflen); + +#if mlib_is_cxx() + // Copy an existing stream + static mlib_ostream from(mlib_ostream o) { return o; } + // Create a stream that appends to an mlib_str + inline static mlib_ostream from(mlib_str* s) noexcept; + // Create a stream that writes to a FILE* + inline static mlib_ostream from(FILE* s) noexcept; + + // Create a stream that appends to an object with a `.append()` method + template ().append(std::declval(), + std::declval()))> + static mlib_ostream from(T& into) { + mlib_ostream ret; + ret.userdata = &into; + ret.write = [](void* userdata, const char* buf, size_t buflen) -> std::size_t { + T& into = *static_cast(userdata); + into.append(buf + 0, buf + buflen); + return buflen; + }; + return ret; + } +#endif // C++ +} mlib_ostream; + +mlib_extern_c_begin(); + +mlib_ostream mlib_ostream_from_fileptr(FILE* out) mlib_noexcept; +mlib_ostream mlib_ostream_from_strptr(mlib_str* out) mlib_noexcept; +inline mlib_ostream _mlib_ostream_copy(mlib_ostream o) mlib_noexcept { return o; } + +#define mlib_ostream_from(Tgt) \ + mlib_generic(mlib_ostream::from, \ + _mlib_ostream_copy, \ + (Tgt), \ + mlib_ostream : _mlib_ostream_copy, \ + mlib_str* : mlib_ostream_from_strptr, \ + FILE* : mlib_ostream_from_fileptr)(Tgt) + +inline size_t mlib_write(mlib_ostream out, mlib_str_view str) mlib_noexcept { + return out.write(out.userdata, str.data, str.len); +} + +#define mlib_write(Into, Str) mlib_write(mlib_ostream_from(Into), mlib_str_view_from(Str)) + +mlib_extern_c_end(); + +#if mlib_is_cxx() +mlib_ostream mlib_ostream::from(mlib_str* s) noexcept { return ::mlib_ostream_from_strptr(s); } +mlib_ostream mlib_ostream::from(FILE* s) noexcept { return ::mlib_ostream_from_fileptr(s); } +#endif // C++ diff --git a/src/mlib/stream.c b/src/mlib/stream.c new file mode 100644 index 0000000..c8cf8ad --- /dev/null +++ b/src/mlib/stream.c @@ -0,0 +1,4 @@ +#include + +extern inline mlib_ostream _mlib_ostream_copy(mlib_ostream o) mlib_noexcept; +extern inline size_t(mlib_write)(mlib_ostream o, mlib_str_view sv) mlib_noexcept; diff --git a/src/mlib/stream.cpp b/src/mlib/stream.cpp new file mode 100644 index 0000000..bd4b7f0 --- /dev/null +++ b/src/mlib/stream.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include + +mlib_ostream mlib_ostream_from_fileptr(FILE* out) noexcept { + mlib_ostream ret; + ret.userdata = out; + ret.write = [](void* userdata, const char* data, size_t buflen) -> std::size_t { + // TODO: Write error? + return std::fwrite(data, 1, buflen, static_cast(userdata)); + }; + return ret; +} + +mlib_ostream mlib_ostream_from_strptr(mlib_str* out) noexcept { + mlib_ostream ret; + ret.userdata = out; + ret.write = [](void* userdata, const char* data, size_t buflen) -> std::size_t { + auto out = static_cast(userdata); + // TODO: Alloc failure + *out = mlib_str_append(*out, mlib_str_view_data(data, buflen)); + return buflen; + }; + return ret; +} From d1d1a32bcecdf7e5e2b1871a5151331a828effda Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 16:56:19 -0700 Subject: [PATCH 10/15] Add basic API for human-readable BSON output. --- include/bson/format.h | 37 ++++++ src/amongoc/wire/proto.cpp | 119 +------------------ src/bson/format.cpp | 234 +++++++++++++++++++++++++++++++++++++ src/bson/format.test.cpp | 62 ++++++++++ tests/sigcheck.test.h | 6 + 5 files changed, 344 insertions(+), 114 deletions(-) create mode 100644 include/bson/format.h create mode 100644 src/bson/format.cpp create mode 100644 src/bson/format.test.cpp diff --git a/include/bson/format.h b/include/bson/format.h new file mode 100644 index 0000000..eb26249 --- /dev/null +++ b/include/bson/format.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include +#include + +mlib_extern_c_begin(); + +/** + * @brief Options to control the output of BSON data + */ +typedef struct bson_fmt_options { + // The initial indent to be added to the output + unsigned initial_indent; + // Base indentation to be added at each newline (except the initial) + unsigned subsequent_indent; + // Additional indentation to be added for each nested object + unsigned nested_indent; +} bson_fmt_options; + +/** + * @brief Format a BSON document into a human-readable string. + * + * @param out The output target for the string + * @param doc The document to be formatted + * @param opts Options to control the formatting + */ +void bson_write_repr(mlib_ostream out, bson_view doc, bson_fmt_options const* opts) mlib_noexcept; + +#define bson_write_repr(...) MLIB_ARGC_PICK(_bson_write_repr, __VA_ARGS__) +#define _bson_write_repr_argc_2(Out, Doc) \ + bson_write_repr(mlib_ostream_from((Out)), bson_view_from((Doc)), NULL) +#define _bson_write_repr_argc_3(Out, Doc, Opts) \ + bson_write_repr(mlib_ostream_from((Out)), bson_view_from((Doc)), (Opts)) + +mlib_extern_c_end(); diff --git a/src/amongoc/wire/proto.cpp b/src/amongoc/wire/proto.cpp index b3538e1..7a24228 100644 --- a/src/amongoc/wire/proto.cpp +++ b/src/amongoc/wire/proto.cpp @@ -1,6 +1,7 @@ #include "./proto.hpp" #include +#include #include #include #include @@ -49,122 +50,12 @@ void wire::trace::message_header(std::string_view prefix, std::fflush(stderr); } -struct _bson_printer { - std::string indent; - - template - void write(fmt::format_string fstr, Args&&... args) { - fmt::print(stderr, fmt::runtime(fstr), args...); - } - - void print(bson_view doc) { - write("{{"); - auto iter = doc.begin(); - if (iter == doc.end()) { - // Empty. Nothing to print - write(" }}"); - return; - } - if (std::next(iter) == doc.end()) { - // Only one element - write(" {:?}: ", iter->key()); - iter->value().visit([this](auto x) { this->print_value(x); }); - write(" }}"); - return; - } - write("\n"); - for (auto ref : doc) { - write("{} {:?}: ", indent, ref.key()); - ref.value().visit([this](auto x) { this->print_value(x); }); - write(",\n"); - } - write("{}}}", indent); - } - - void print_value(bson_array_view arr) { - write("["); - auto iter = arr.begin(); - if (iter == arr.end()) { - write("]"); - return; - } - write("\n"); - auto indented = _bson_printer{indent + " "}; - for (auto ref : arr) { - write("{} ", indent); - ref.value().visit([&](auto x) { indented.print_value(x); }); - write(",\n"); - } - write(" {}]", indent); - } - - void print_value(std::string_view s) { write("{:?}", s); } - void print_value(bson_symbol_view s) { write("Symbol({:?})", std::string_view(s.utf8)); } - - auto _as_formattable_time_point(std::int64_t utc_ms_offset) { -#if FMT_USE_UTC_TIME - // C++20 UTC time point is supported - return std::chrono::utc_clock::time_point(std::chrono::milliseconds(utc_ms_offset)); -#else - // Fall back to the system clock. This is not certain to give the correct answer, - // but we're just logging, not saving the world - return std::chrono::system_clock::time_point(std::chrono::milliseconds(utc_ms_offset)); -#endif - } - - void print_value(::bson_datetime dt) { - auto tp = _as_formattable_time_point(dt.utc_ms_offset); - write("Datetime⟨{:%c}⟩", tp); - } - - void print_value(::bson_timestamp ts) { - auto tp = _as_formattable_time_point(ts.utc_sec_offset); - write("Timestamp(⟨{:%c}⟩ : {})", tp, ts.increment); - } - void print_value(::bson_code_view c) { write("Code({:?})", std::string_view(c.utf8)); } - - void print_value(::bson_decimal128) { write("[[Unimplemented: Decimal128 printing]]"); } - - void print_value(bson_eod) { - assert(false && "Should never be called. Tried to print-trace a phony EOD element."); - } - void print_value(bool b) { write("{}", b); } - void print_value(std::int32_t i) { write("{}:i32", i); } - void print_value(std::int64_t i) { write("{}:i64", i); } - void print_value(std::uint64_t i) { write("{}:u64", i); } - void print_value(double i) { write("{}:f64", i); } - void print_value(bson::null) { write("null"); } - void print_value(bson::undefined) { write("undefined"); } - void print_value(bson::minkey) { write("[[min key]]"); } - void print_value(bson::maxkey) { write("[[max key]]"); } - void print_value(bson_view subdoc) { _bson_printer{indent + " "}.print(subdoc); } - void print_value(bson_dbpointer_view dbp) { - write("DBPointer(\"{}\", ", std::string_view(dbp.collection)); - print_value(dbp.object_id); - write(")"); - } - void print_value(bson_oid oid) { - write("ObjectID("); - print_bytes(oid.bytes); - write(")"); - } - void print_bytes(auto&& seq) { - for (auto n : seq) { - write("{:0>2x}", std::uint8_t(std::byte(n))); - } - } - void print_value(bson_regex_view rx) { - write("/{}/{}", std::string_view(rx.regex), std::string_view(rx.options)); - } - void print_value(bson_binary_view bin) { - write("Binary({}, ", bin.subtype); - print_bytes(std::ranges::subrange(bin.data, bin.data + bin.data_len)); - } -}; - void wire::trace::message_body_section(int nth, bson_view body) { fmt::print(stderr, " Section #{} body: ", nth); - _bson_printer{" "}.print(body); + bson_fmt_options opts{}; + opts.subsequent_indent = 2; + opts.nested_indent = 2; + bson_write_repr(stderr, body, &opts); fmt::print(stderr, "\n"); } diff --git a/src/bson/format.cpp b/src/bson/format.cpp new file mode 100644 index 0000000..79ed8b9 --- /dev/null +++ b/src/bson/format.cpp @@ -0,0 +1,234 @@ +#include +#include +#include + +#include + +mlib_diagnostic_push(); +mlib_gcc_warning_disable("-Wstringop-overflow"); +#include +mlib_diagnostic_pop(); + +namespace { + +struct mlib_os_writebuf { + mlib_ostream out; + + // Small buffer to hold chars pending flush + char smallbuf[512] = {0}; + // number of chars in the smallbuf + size_t nbuf = 0; + // Add a char to the output + void put(char c) { + smallbuf[nbuf] = c; + ++nbuf; + if (nbuf == sizeof smallbuf) { + // Buffer is full. Send it + flush(); + } + } + // Write pending output through the stream abstraction. + void flush() { + mlib_write(out, std::string_view(smallbuf, nbuf)); + nbuf = 0; + } +}; + +// Output iterator that writes to an mos_wr_buf +struct writer_iter { + mlib_os_writebuf* into; + + writer_iter& operator++() noexcept { return *this; } + writer_iter operator++(int) noexcept { return *this; } + writer_iter& operator*() noexcept { return *this; } + void operator=(char c) noexcept { into->put(c); } +}; + +template +struct bson_writer { + O _output; + bson_fmt_options _opts; + unsigned depth = 0; + + bool multiline() { return this->_opts.nested_indent != 0; } + + bson_writer nested() { + auto dup = *this; + dup.depth++; + return dup; + } + void add_newline() { + write("\n"); + add_hspace(this->_opts.subsequent_indent); + add_hspace(this->_opts.nested_indent * this->depth); + } + + template + void write(fmt::format_string fstr, Args&&... args) { + fmt::format_to(_output, fmt::runtime(fstr), args...); + } + + void add_hspace(std::size_t count) { + const auto spaces = " "; + while (count) { + auto len = (std::min)(strlen(spaces), count); + write("{}", std::string_view(spaces, len)); + count -= len; + } + } + + void top_write(bson::view doc) { + add_hspace(_opts.initial_indent); + write_value(doc); + } + + void write_doc(bson::view doc) { + write("{{"); + auto iter = doc.begin(); + if (iter == doc.end()) { + // Empty doc. No line break + write(" }}"); + return; + } + if (std::next(iter) == doc.end() && iter->type() != bson_type_document + && iter->type() != bson_type_array) { + // Only one value + write(" {:?}: ", iter->key()); + write_value(*iter); + write(" }}"); + return; + } + if (this->multiline()) { + for (auto ref : doc) { + this->add_newline(); + add_hspace(this->_opts.nested_indent); + write("{:?}: ", ref.key()); + this->nested().write_value(ref); + write(","); + } + this->add_newline(); + write("}}"); + } else { + while (iter != doc.end()) { + write(" {:?}: ", iter->key()); + this->write_value(*iter); + ++iter; + if (iter != doc.end()) { + write(","); + } + } + write(" }}"); + } + } + + void write_value(bson_array_view arr) { + write("["); + auto iter = arr.begin(); + if (iter == arr.end()) { + write("]"); + return; + } + if (this->multiline()) { + for (auto ref : arr) { + this->add_newline(); + add_hspace(this->_opts.nested_indent); + this->nested().write_value(ref); + write(","); + } + this->add_newline(); + write("]"); + } else { + while (iter != arr.end()) { + write(" ", iter->key()); + this->write_value(*iter); + ++iter; + if (iter != arr.end()) { + write(","); + } + } + write(" ]"); + } + } + + void write_value(bson_iterator::reference ref) { + ref.value().visit([&](auto x) { this->write_value(x); }); + } + + void write_value(std::string_view sv) { write("{:?}", sv); } + void write_value(bson_symbol_view s) { write("Symbol({:?})", std::string_view(s.utf8)); } + + auto _as_formattable_time_point(std::int64_t utc_ms_offset) { +#if FMT_USE_UTC_TIME + // C++20 UTC time point is supported + return std::chrono::utc_clock::time_point(std::chrono::milliseconds(utc_ms_offset)); +#else + // Fall back to the system clock. This is not certain to give the correct answer, + // but we're just logging, not saving the world + return std::chrono::system_clock::time_point(std::chrono::milliseconds(utc_ms_offset)); +#endif + } + + void write_value(::bson_datetime dt) { + auto tp = _as_formattable_time_point(dt.utc_ms_offset); + write("Datetime⟨{:%c}⟩", tp); + } + void write_value(::bson_timestamp ts) { + auto tp = _as_formattable_time_point(ts.utc_sec_offset); + write("Timestamp(⟨{:%c}⟩ : {})", tp, ts.increment); + } + void write_value(::bson_code_view c) { write("Code({:?})", std::string_view(c.utf8)); } + void write_value(::bson_decimal128) { write("[[Unimplemented: Decimal128 printing]]"); } + + void write_value(bson_eod) { + assert(false && "Should never be called. Tried to format a phony EOD element."); + } + void write_value(bool b) { write("{}", b); } + void write_value(std::int32_t i) { write("{}:i32", i); } + void write_value(std::int64_t i) { write("{}:i64", i); } + void write_value(std::uint64_t i) { write("{}:u64", i); } + void write_value(double i) { write("{}:f64", i); } + void write_value(bson::null) { write("null"); } + void write_value(bson::undefined) { write("undefined"); } + void write_value(bson::minkey) { write("[[min key]]"); } + void write_value(bson::maxkey) { write("[[max key]]"); } + + void write_value(bson_view subdoc) { this->write_doc(subdoc); } + void write_value(bson_dbpointer_view dbp) { + write("DBPointer(\"{}\", ", std::string_view(dbp.collection)); + write_value(dbp.object_id); + write(")"); + } + void write_value(bson_oid oid) { + write("ObjectID("); + print_bytes(oid.bytes); + write(")"); + } + void print_bytes(auto&& seq) { + for (auto n : seq) { + write("{:0>2x}", std::uint8_t(std::byte(n))); + } + } + void write_value(bson_regex_view rx) { + write("/{}/{}", std::string_view(rx.regex), std::string_view(rx.options)); + } + void write_value(bson_binary_view bin) { + write("Binary(subtype {}, bytes 0x", bin.subtype); + print_bytes(std::ranges::subrange(bin.data, bin.data + bin.data_len)); + write(")"); + } +}; + +} // namespace + +void(bson_write_repr)(mlib_ostream out, bson_view doc, bson_fmt_options const* opts) noexcept { + mlib_os_writebuf o{out}; + bson_writer wr{writer_iter{&o}, + opts ? *opts + : bson_fmt_options{ + .initial_indent = 0, + .subsequent_indent = 0, + .nested_indent = 2, + }}; + wr.top_write(doc); + o.flush(); +} diff --git a/src/bson/format.test.cpp b/src/bson/format.test.cpp new file mode 100644 index 0000000..6f9aeba --- /dev/null +++ b/src/bson/format.test.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include + +#include + +#include + +TEST_CASE("bson/format empty") { + bson::document empty{::mlib_default_allocator}; + mlib_str s = mlib_str_new().str; + ::bson_write_repr(&s, empty); + CHECK(s == "{ }"); + mlib_str_delete(s); +} + +TEST_CASE("bson/format to std::string") { + bson::document empty{::mlib_default_allocator}; + std::string s; + ::bson_write_repr(s, empty); + CHECK(s == "{ }"); +} + +TEST_CASE("bson/format non-empty") { + auto doc = bson::make::doc(bson::make::pair("hey", 42)).build(::mlib_default_allocator); + std::string s; + ::bson_write_repr(s, doc); + CHECK(s == R"({ "hey": 42:i32 })"); + bson::mutator(doc).emplace_back("foo", "bar"); + s.clear(); + ::bson_write_repr(s, doc); + CHECK(s == R"({ + "hey": 42:i32, + "foo": "bar", +})"); + ; +} + +TEST_CASE("bson/format nested") { + using namespace bson::make; + auto doct = doc(pair("doc", doc(pair("foo", "bar"), pair("baz", "quux")))) + .build(::mlib_default_allocator); + std::string s; + ::bson_write_repr(s, doct); + CHECK(s == R"({ + "doc": { + "foo": "bar", + "baz": "quux", + }, +})"); +} + +TEST_CASE("bson/format oneline") { + using namespace bson::make; + auto doct = doc(pair("doc", doc(pair("foo", "bar"), pair("baz", "quux")))) + .build(::mlib_default_allocator); + std::string s; + bson_fmt_options opts{}; + ::bson_write_repr(s, doct, &opts); + CHECK(s == R"({ "doc": { "foo": "bar", "baz": "quux" } })"); +} diff --git a/tests/sigcheck.test.h b/tests/sigcheck.test.h index 6a1bd20..619383f 100644 --- a/tests/sigcheck.test.h +++ b/tests/sigcheck.test.h @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -211,6 +212,11 @@ static inline void amongoc_test_all_signatures() { op = GLOBAL_SCOPE amongoc_tie(some_emitter, &some_userdata); op = GLOBAL_SCOPE amongoc_tie(some_emitter, &status, &some_userdata); op = GLOBAL_SCOPE amongoc_tie(some_emitter, &status, &some_userdata, mlib_default_allocator); + + mlib_ostream os = mlib_ostream_from(stderr); + GLOBAL_SCOPE bson_write_repr(&some_string, some_bson_doc); + GLOBAL_SCOPE bson_write_repr(os, some_bson_doc); + GLOBAL_SCOPE bson_write_repr(stderr, some_bson_doc); } mlib_diagnostic_pop(); From 9f812e16ee3a163f2d3b1bfc564433c9db6619ab Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 16:53:50 -0700 Subject: [PATCH 11/15] Use our BSON printer in the examples --- docs/how-to/communicate.example.c | 70 ++----------------------------- docs/how-to/communicate.rst | 2 +- 2 files changed, 5 insertions(+), 67 deletions(-) diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index e35dbef..ae1ac78 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -1,10 +1,9 @@ #include -#include "bson/view.h" +#include #include #include - -#include "mlib/str.h" +#include /** * @brief Shared state for the application. This is passed through the app as pointer stored @@ -15,11 +14,6 @@ typedef struct app_state { amongoc_client* client; } app_state; -/** - * @brief Write the content of a BSON document in JSON-like format to the given output - */ -static void print_bson(FILE* into, bson_view doc, mlib_str_view indent); - /** after_hello() * @brief Handle the `hello` response from the server * @@ -32,7 +26,7 @@ amongoc_box after_hello(amongoc_box state_ptr, amongoc_status*, amongoc_box resp bson_view resp = bson_view_from(amongoc_box_cast(bson_doc, resp_data)); // Just print the response message fprintf(stdout, "Got response: "); - print_bson(stdout, resp, mlib_str_view_from("")); + bson_write_repr(stderr, resp); fputs("\n", stdout); amongoc_box_destroy(resp_data); return amongoc_nil; @@ -101,6 +95,7 @@ int main(int argc, char const* const* argv) { amongoc_client_delete(state.client); amongoc_default_loop_destroy(&loop); + // Final status amongoc_if_error (status, msg) { fprintf(stderr, "An error occurred: %s\n", msg); return 2; @@ -110,60 +105,3 @@ int main(int argc, char const* const* argv) { } } // end. - -static void print_bson(FILE* into, bson_view doc, mlib_str_view indent) { - fprintf(into, "{\n"); - bson_foreach(it, doc) { - mlib_str_view str = bson_key(it); - fprintf(into, "%*s \"%s\": ", (int)indent.len, indent.data, str.data); - bson_value_ref val = bson_iterator_value(it); - switch (val.type) { - case bson_type_eod: - case bson_type_double: - fprintf(into, "%f,\n", val.double_); - break; - case bson_type_utf8: - fprintf(into, "\"%s\",\n", val.utf8.data); - break; - case bson_type_document: - case bson_type_array: { - mlib_str i2 = mlib_str_append(indent, " "); - bson_view subdoc = bson_iterator_value(it).document; - print_bson(into, subdoc, mlib_str_view_from(i2)); - mlib_str_delete(i2); - fprintf(into, ",\n"); - break; - } - case bson_type_undefined: - fprintf(into, "[undefined],\n"); - break; - case bson_type_bool: - fprintf(into, val.bool_ ? "true,\n" : "false,\n"); - break; - case bson_type_null: - fprintf(into, "null,\n"); - break; - case bson_type_int32: - fprintf(into, "%d,\n", val.int32); - break; - case bson_type_int64: - fprintf(into, "%ld,\n", val.int64); - break; - case bson_type_timestamp: - case bson_type_decimal128: - case bson_type_maxkey: - case bson_type_minkey: - case bson_type_oid: - case bson_type_binary: - case bson_type_datetime: - case bson_type_regex: - case bson_type_dbpointer: - case bson_type_code: - case bson_type_symbol: - case bson_type_codewscope: - fprintf(into, "[[printing unimplemented for this type]],\n"); - break; - } - } - fprintf(into, "%*s}", (int)indent.len, indent.data); -} diff --git a/docs/how-to/communicate.rst b/docs/how-to/communicate.rst index 9094fa1..149e025 100644 --- a/docs/how-to/communicate.rst +++ b/docs/how-to/communicate.rst @@ -240,7 +240,7 @@ Print the Final Result .. literalinclude:: communicate.example.c :lineno-match: - :start-at: if_error + :start-at: // Final status :end-before: end. Finally, we inspect the `amongoc_status` that was produced by our operation and From c73b51403fa7b7951dcbd77c496ec122c6aaa410 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 16:59:07 -0700 Subject: [PATCH 12/15] Add BSON #includes to the everything-header --- include/amongoc/amongoc.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/amongoc/amongoc.h b/include/amongoc/amongoc.h index 42ea605..0536871 100644 --- a/include/amongoc/amongoc.h +++ b/include/amongoc/amongoc.h @@ -11,3 +11,8 @@ #include "./emitter.h" #include "./handler.h" #include "./operation.h" + +#include +#include +#include +#include From 75a8cfad200f9450d85aa7c072ab10243c587208 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 17:55:10 -0700 Subject: [PATCH 13/15] Tutorial: Reading data from a collection --- docs/conf.py | 1 + docs/learn/index.rst | 1 + docs/learn/read.example.c | 115 ++++++++++++++++++ docs/learn/read.rst | 243 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+) create mode 100644 docs/learn/read.example.c create mode 100644 docs/learn/read.rst diff --git a/docs/conf.py b/docs/conf.py index dafca60..0579a22 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -338,6 +338,7 @@ def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> No "default_loop", "client", "collection", + "cursor", ) ), *( diff --git a/docs/learn/index.rst b/docs/learn/index.rst index 0f5e3d0..7fa543a 100644 --- a/docs/learn/index.rst +++ b/docs/learn/index.rst @@ -10,3 +10,4 @@ Tutorials box connect write + read diff --git a/docs/learn/read.example.c b/docs/learn/read.example.c new file mode 100644 index 0000000..563ce41 --- /dev/null +++ b/docs/learn/read.example.c @@ -0,0 +1,115 @@ +#include "amongoc/async.h" +#include "amongoc/box.h" +#include "amongoc/client.h" +#include "amongoc/collection.h" +#include "amongoc/default_loop.h" +#include + +#include "bson/format.h" + +// Application state +typedef struct { + // The client + amongoc_client* client; + // The collection we are reading from + amongoc_collection* collection; + // The name of the database we read from + const char* db_name; + // The name of the collection within the database + const char* coll_name; + int batch_num; +} app_state; + +static amongoc_emitter on_connect(amongoc_box state, amongoc_status status, amongoc_box client); + +int main(int argc, char** argv) { + // Program parameters + if (argc != 4) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 2; + } + // The server URI + const char* const uri = argv[1]; + + // Initialize our state + app_state state = { + .db_name = argv[2], + .coll_name = argv[3], + }; + + amongoc_loop loop; + amongoc_if_error (amongoc_default_loop_init(&loop), msg) { + fprintf(stderr, "Error initializing the event loop: %s\n", msg); + return 1; + } + + // Connect + amongoc_emitter em = amongoc_client_new(&loop, uri); + + // Set the continuation + em = amongoc_let(em, amongoc_async_forward_errors, amongoc_box_pointer(&state), on_connect); + + // Run the program and collect the result + amongoc_status status; + amongoc_operation op = amongoc_tie(em, &status); + amongoc_start(&op); + amongoc_default_loop_run(&loop); + amongoc_operation_delete(op); + amongoc_collection_delete(state.collection); + amongoc_client_delete(state.client); + amongoc_default_loop_destroy(&loop); + + // Check for errors + amongoc_if_error (status, msg) { + fprintf(stderr, "Error: %s\n", msg); + return 1; + } +} + +static amongoc_emitter on_find(amongoc_box state_, amongoc_status status, amongoc_box cursor_); + +// Continuation after connection completes +static amongoc_emitter on_connect(amongoc_box state_, amongoc_status status, amongoc_box client) { + // We don't use the status + (void)status; + // Store the client object + app_state* state = amongoc_box_cast(app_state*, state_); + amongoc_box_take(state->client, client); + // Create a new collection handle + state->collection = amongoc_collection_new(state->client, state->db_name, state->coll_name); + // Initiate a read operation. + amongoc_emitter em = amongoc_find(state->collection, bson_view_null, NULL); + return amongoc_let(em, amongoc_async_forward_errors, state_, on_find); +} + +// Continuation when we get data +static amongoc_emitter on_find(amongoc_box state_, amongoc_status status, amongoc_box cursor_) { + // We don't use the status + (void)status; + app_state* state = amongoc_box_cast(app_state*, state_); + // Extract the cursor + amongoc_cursor cursor; + amongoc_box_take(cursor, cursor_); + + // Print the data + fprintf(stderr, + "Got results from database '%s' collection '%s', batch %d: ", + state->db_name, + state->coll_name, + state->batch_num); + bson_write_repr(stderr, cursor.records); + fprintf(stderr, "\n"); + state->batch_num++; + + // Do we have more data? + if (cursor.cursor_id) { + // More to find + amongoc_emitter em = amongoc_cursor_next(cursor); + return amongoc_let(em, amongoc_async_forward_errors, state_, on_find); + } else { + // Done + amongoc_cursor_delete(cursor); + return amongoc_just(); + } +} +// end:on_find diff --git a/docs/learn/read.rst b/docs/learn/read.rst new file mode 100644 index 0000000..c8f461c --- /dev/null +++ b/docs/learn/read.rst @@ -0,0 +1,243 @@ +############ +Reading Data +############ + +This tutorial will show the basics of reading data from a MongoDB server. It +assumes that you have read the :doc:`connect` tutorial, and that you have a +MongoDB server that contains data you wish to read. + + +Declaring Application State Type +################################ + +This program will require that certain objects outlive the sub-operations, so we +will need to persist them outside the event loop and pass them to our +continuation. We declare a simple aggregate type to hold these objects: + +.. literalinclude:: read.example.c + :caption: Application State Struct + :start-at: // Application state + :end-at: app_state; + :lineno-match: + + +Parsing Command Arguments +######################### + +Our program will take three parameters: A MongoDB server URI, a database name, +and a collection name. We can parse those as the start of ``main``: + +.. literalinclude:: read.example.c + :caption: Argument Handling + :start-at: int main + :end-at: }; + :lineno-match: + +We store the database name and the collection name in the shared state struct +so that we can access it in subsequent operations. + + +Starting with a Connection +########################## + +We next do the basics of setting up an event loop and creating a connection +:term:`emitter`: + +.. literalinclude:: read.example.c + :caption: Setting up an event loop + :start-at: amongoc_loop loop; + :end-at: amongoc_client_new + :lineno-match: + + +Create the First Continuation +############################# + +We have the pending connection object and the application state, so now we can +set the first continuation: + +.. literalinclude:: read.example.c + :caption: The first continuation + :start-at: Set the continuation + :end-at: on_connect); + :lineno-match: + + +The arguments are as follows: + +1. Passing the emitter we got from `amongoc_client_new`, and replacing it with + the new operation emitter returned by `amongoc_let`. +2. `amongoc_async_forward_errors` is a behavior control flag for `amongoc_let` + that tells the operation to skip our continuation if the input operation + generates an error. +3. We pass the address of the ``state`` object by wrapping it with + `amongoc_box_pointer`, passing it as the ``userdata`` parameter of + `amongoc_let`. +4. ``on_connect`` is the name of the function that will handle the continuation. + + +Define the Continuation +####################### + +Now we can look at the definition of ``on_connect``: + +.. literalinclude:: read.example.c + :caption: Continuation signature + :start-at: // Continuation after connection + :end-at: (void)status; + :lineno-match: + +The ``state_`` parameter is the userdata box that was given to `amongoc_let` in +the previous section. The ``client`` parameter is a box that contains the +`amongoc_client` pointer from the connection operation. The ``status`` parameter +here is the status of the connection operation, but we don't care about this +here since we used `amongoc_async_forward_errors` to ask `amongoc_let` to skip +this function if the status would otherwise indicate an error (i.e. +``on_connect`` will only be called if the connection actually succeeds). + + +Update the Application State +**************************** + +``on_connect`` is passed the pointer to the application state as well as a box +containing a pointer to the `amongoc_client`. We can extract the box value and +store the client pointer within our application state struct: + +.. literalinclude:: read.example.c + :caption: Update the Application State + :start-at: // Store the client + :end-at: amongoc_box_take + :lineno-match: + +We store it in the application state object so that we can later delete the +client handle when the program completes. + + +Create a Collection Handle +************************** + +Now that we have a valid client, we can create a handle to a collection on +the server: + +.. literalinclude:: read.example.c + :caption: Set up the Collection + :start-at: Create a new collection handle + :end-at: } + :lineno-match: + +`amongoc_collection_new` does not actually communicate with the server: It only +creates a client-side handle that can be used to perform operations related to +that collection. + +We store the returned collection handle in the state struct so that we can later +delete it. + + +Initiate a Read Operation +************************* + +There are several different reading operations and reading parameters. For this +program, we'll simply do a "find all" operation: + +.. literalinclude:: read.example.c + :caption: Start a "Find All" Operation + :start-at: Initiate a read + :end-at: return + :lineno-match: + +The `amongoc_find` function will return a new `amongoc_emitter` that represents +the pending read from a collection. The first parameter :cpp:`state->collection` +is a pointer to a collection handle. The second parameter `bson_view_null` is a +filter for the find operation. In this case, we are passing no filter, which is +equivalent to an empty filter, telling the database that we want to find all +documents in the collection. The third parameter is a set of common named +parameters for find operations. In this case, we are passing :cpp:`NULL`, +because we don't care to specify any more parameters. + +We attach a second continuation using `amongoc_let` to a function ``on_find``. + + +Read from The Cursor +******************** + +When the `amongoc_find` emitter resolves, it resolves with an `amongoc_cursor` +object. We'll extract that in ``on_find``: + +.. literalinclude:: read.example.c + :caption: Handle Data + :start-at: Continuation when we get data + :end-at: box_take + :lineno-match: + +We can print out the cursor's batch of data: + +.. literalinclude:: read.example.c + :caption: Print the Records + :start-at: Print the data + :end-at: batch_num++ + :lineno-match: + + +Read Some More +************** + +A single read may not return the full set of data, so we may need to initiate +a subsequent read with `amongoc_cursor_next`: + +.. literalinclude:: read.example.c + :caption: Check for more + :start-at: Do we have more + :end-before: end:on_find + :lineno-match: + +By passing `on_find` to `amongoc_let` again, we create a loop that continually +calls ``on_find`` until the cursor ID is zero (indicating a finished read). + +If we have no more data, we destroy the cursor object and return a null result +immediately with `amongoc_just`. + + +Tie, Start, and Run the Operation +################################# + +Going back to ``main``, we can now tie the operation, start it, and run the +event loop: + +.. literalinclude:: read.example.c + :caption: Run the Program + :start-at: Run the program + :end-at: loop_destroy + :lineno-match: + +`amongoc_tie` tells the emitter to store its final status and result value in +the given destinations, and returns an operation that can be initiated with +`amongoc_start`. We then give control to the event loop with +`amongoc_default_loop_run`. After this returns, the operation object is +finished, so we delete it with `amongoc_operation_delete`. + +We are also finished with the collection handle and the client, so we delete +those here as well. Those struct members will have been filled in by +``on_connect``. + + +Check for Errors +################ + +If any sub-operation failed, the error status will be propagated to the final +status, so we check that before exiting: + +.. literalinclude:: read.example.c + :caption: Check for Errors + :start-at: Check for errors + :end-at: } + :lineno-match: + + +The Full Program +################ + +Here is the full sample program, in its entirety: + +.. literalinclude:: read.example.c + :caption: ``read.example.c`` + :linenos: From 93644fcc9efc5cfb6a9a8fbf2c0ecf860f98431f Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Fri, 24 Jan 2025 17:56:30 -0700 Subject: [PATCH 14/15] Minor formatting --- .clang-format | 2 ++ Makefile | 2 +- docs/learn/bson/bson.example.cpp | 12 ++++++------ docs/learn/read.example.c | 7 ------- src/bson/view.test.cpp | 6 +++--- tools/include-fixup.py | 2 +- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.clang-format b/.clang-format index 6b89373..8a2b3a5 100644 --- a/.clang-format +++ b/.clang-format @@ -83,6 +83,8 @@ InsertNewlineAtEOF: true IfMacros: - mlib_math_catch - amongoc_if_error +ForEachMacros: + - bson_foreach AllowBreakBeforeNoexceptSpecifier: Always --- # For some reason, Clang sees some files as Objective-C. Add this section just to appease it. diff --git a/Makefile b/Makefile index ee4145a..4e5dcee 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ build: test: build cmake -E chdir "$(BUILD_DIR)" ctest -C "$(CONFIG)" --output-on-failure -j8 -all_sources := $(shell find $(THIS_DIR)/src/ $(THIS_DIR)/include/ $(THIS_DIR)/tests/ -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp') +all_sources := $(shell find $(THIS_DIR)/src/ $(THIS_DIR)/include/ $(THIS_DIR)/tests/ $(THIS_DIR)/docs/ -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp') format-check: poetry-install $(POETRY) run python tools/include-fixup.py --check $(POETRY) run $(MAKE) _format-check diff --git a/docs/learn/bson/bson.example.cpp b/docs/learn/bson/bson.example.cpp index 70c6b85..c382850 100644 --- a/docs/learn/bson/bson.example.cpp +++ b/docs/learn/bson/bson.example.cpp @@ -1,8 +1,8 @@ -#include "bson/iterator.h" -#include "bson/mut.h" -#include "bson/types.h" -#include "bson/view.h" #include +#include +#include +#include +#include // ex: [create-c] #include @@ -157,7 +157,7 @@ void do_loop(bson_view data) { // ex: [foreach] void foreach_loop(bson_view data) { - bson_foreach(it, data) { + bson_foreach (it, data) { // `it` refers to the current element. printf("Got an element: %s\n", bson_key(it).data); } @@ -207,7 +207,7 @@ bool subdoc_iter(bson_view top) { } // Iterate over each element of the array - bson_foreach(sub_iter, val.array) { + bson_foreach (sub_iter, val.array) { if (bson_iterator_get_error(sub_iter)) { // Iterating over a child element encountered an error fprintf(stderr, diff --git a/docs/learn/read.example.c b/docs/learn/read.example.c index 563ce41..41ed9dc 100644 --- a/docs/learn/read.example.c +++ b/docs/learn/read.example.c @@ -1,12 +1,5 @@ -#include "amongoc/async.h" -#include "amongoc/box.h" -#include "amongoc/client.h" -#include "amongoc/collection.h" -#include "amongoc/default_loop.h" #include -#include "bson/format.h" - // Application state typedef struct { // The client diff --git a/src/bson/view.test.cpp b/src/bson/view.test.cpp index 8ef2872..2f6983b 100644 --- a/src/bson/view.test.cpp +++ b/src/bson/view.test.cpp @@ -174,7 +174,7 @@ TEST_CASE("bson/view/foreach/break") { bson_iterator last_seen{}; int nth = 0; // There are three elements, but we stop at two - bson_foreach(iter, v) { + bson_foreach (iter, v) { last_seen = iter; ++nth; if (nth == 2) { @@ -219,7 +219,7 @@ TEST_CASE("bson/view/foreach/error iterator") { int nth = 0; bool got_error = false; - bson_foreach(iter, v) { + bson_foreach (iter, v) { switch (nth++) { case 0: CHECK(iter->key() == "r"); @@ -252,7 +252,7 @@ TEST_CASE("bson/view/foreach/Once evaluation") { return doc; }; - bson_foreach(it, get()) { + bson_foreach (it, get()) { (void)it; // Empty } diff --git a/tools/include-fixup.py b/tools/include-fixup.py index 4b25fa6..0b32c14 100644 --- a/tools/include-fixup.py +++ b/tools/include-fixup.py @@ -23,7 +23,7 @@ "**/*.hpp", ] -dirs = ["src/", "include/"] +dirs = ["src/", "include/", "tests/", "docs/"] dirs = map(Path, dirs) source_files = itertools.chain.from_iterable( From 8c40a59d7785108bfcff01725db6ae04c15ed223 Mon Sep 17 00:00:00 2001 From: vector-of-bool Date: Thu, 30 Jan 2025 11:43:46 -0700 Subject: [PATCH 15/15] Wording and tweaks from PR comments Co-authored-by: Kevin Albertson --- docs/how-to/communicate.example.c | 2 +- docs/learn/read.rst | 2 +- docs/ref/status.rst | 4 ++-- src/bson/format.cpp | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/how-to/communicate.example.c b/docs/how-to/communicate.example.c index ae1ac78..880ff98 100644 --- a/docs/how-to/communicate.example.c +++ b/docs/how-to/communicate.example.c @@ -26,7 +26,7 @@ amongoc_box after_hello(amongoc_box state_ptr, amongoc_status*, amongoc_box resp bson_view resp = bson_view_from(amongoc_box_cast(bson_doc, resp_data)); // Just print the response message fprintf(stdout, "Got response: "); - bson_write_repr(stderr, resp); + bson_write_repr(stdout, resp); fputs("\n", stdout); amongoc_box_destroy(resp_data); return amongoc_nil; diff --git a/docs/learn/read.rst b/docs/learn/read.rst index c8f461c..6797700 100644 --- a/docs/learn/read.rst +++ b/docs/learn/read.rst @@ -122,7 +122,7 @@ the server: .. literalinclude:: read.example.c :caption: Set up the Collection :start-at: Create a new collection handle - :end-at: } + :end-before: Initiate a read operation. :lineno-match: `amongoc_collection_new` does not actually communicate with the server: It only diff --git a/docs/ref/status.rst b/docs/ref/status.rst index d772f0b..b238488 100644 --- a/docs/ref/status.rst +++ b/docs/ref/status.rst @@ -178,7 +178,7 @@ Functions & Macros :param MsgVar: This argument should be a plain identifier, which will be declared within the scope of the statement as the :term:`C string` for the status. :param StatusVar: If provided, a variable of type `amongoc_status` will be declared within - the statment scope that captures the value of the ``Status`` argument. + the statement scope that captures the value of the ``Status`` argument. .. hint:: @@ -229,7 +229,7 @@ Status Categories return |S| without inspecting `buf`. 2. If the message |M| needs to be dynamically generated and `buf` is not null, generate the message string in `buf`, ensuring that `buf` contains - a nul terminator at :expr:`buf[buflen-1]` (use of ``snprintf`` is + a nul terminator. The written length with nul terminator must not exceed :expr:`buflen` (use of ``snprintf`` is recommended). Return `buf`. 3. Otherwise, return a fallback message string or a null pointer. diff --git a/src/bson/format.cpp b/src/bson/format.cpp index 79ed8b9..7dad7fa 100644 --- a/src/bson/format.cpp +++ b/src/bson/format.cpp @@ -185,7 +185,6 @@ struct bson_writer { void write_value(bool b) { write("{}", b); } void write_value(std::int32_t i) { write("{}:i32", i); } void write_value(std::int64_t i) { write("{}:i64", i); } - void write_value(std::uint64_t i) { write("{}:u64", i); } void write_value(double i) { write("{}:f64", i); } void write_value(bson::null) { write("null"); } void write_value(bson::undefined) { write("undefined"); }