Communicating with a Server#

This how-to guide will walk through the follow example program:

communicate.example.c#
  1#include <amongoc/amongoc.h>
  2
  3#include <bson/format.h>
  4#include <bson/iterator.h>
  5#include <bson/mut.h>
  6#include <bson/view.h>
  7
  8/**
  9 * @brief Shared state for the application. This is passed through the app as pointer stored
 10 * in a box
 11 */
 12typedef struct app_state {
 13    // The connection to a server
 14    amongoc_client* client;
 15} app_state;
 16
 17/** after_hello()
 18 * @brief Handle the `hello` response from the server
 19 *
 20 * @param state_ptr Pointer to the `app_state`
 21 * @param resp_data A `bson_mut` object that contains the response message
 22 * @return amongoc_box Returns `amongoc_nil`
 23 */
 24amongoc_box after_hello(amongoc_box state_ptr, amongoc_status*, amongoc_box resp_data) {
 25    (void)state_ptr;
 26    bson_view resp = bson_view_from(amongoc_box_cast(bson_doc, resp_data));
 27    // Just print the response message
 28    fprintf(stdout, "Got response: ");
 29    bson_write_repr(stdout, resp);
 30    fputs("\n", stdout);
 31    amongoc_box_destroy(resp_data);
 32    return amongoc_nil;
 33}
 34// end.
 35
 36/** after_connect_say_hello()
 37 * @brief Handle the connection to a server. Sends a "hello" message
 38 *
 39 * @param state_ptr Pointer to the `app_state`
 40 * @param cl_box An `amongoc_client*`
 41 * @return amongoc_emitter
 42 */
 43amongoc_emitter after_connect_say_hello(amongoc_box state_ptr, amongoc_status, amongoc_box cl_box) {
 44    printf("Connected to server\n");
 45    // Store the connection in our app state
 46    amongoc_box_take(amongoc_box_cast(app_state*, state_ptr)->client, cl_box);
 47
 48    // Create a "hello" command
 49    bson_doc doc = bson_new();
 50    bson_mut mut = bson_mutate(&doc);
 51    bson_insert(&mut, "hello", "1");
 52    bson_insert(&mut, "$db", "test");
 53    amongoc_emitter em = amongoc_client_command(amongoc_box_cast(app_state*, state_ptr)->client,
 54                                                bson_view_from(mut));
 55    bson_delete(doc);
 56
 57    em = amongoc_then(em, amongoc_async_forward_errors, state_ptr, after_hello);
 58    return em;
 59}
 60// end.
 61
 62int main(int argc, char const* const* argv) {
 63    if (argc != 2) {
 64        fprintf(stderr, "Usage: %s <uri>\n", argv[0]);
 65        return 1;
 66    }
 67    const char* const uri = argv[1];
 68
 69    amongoc_loop   loop;
 70    amongoc_status status = amongoc_default_loop_init(&loop);
 71    amongoc_if_error (status, msg) {
 72        fprintf(stderr, "Error setting up the event loop: %s\n", msg);
 73        return 2;
 74    }
 75
 76    struct app_state state = {0};
 77
 78    amongoc_emitter em = amongoc_client_new(&loop, uri);
 79    struct timespec dur;
 80    dur.tv_sec  = 5;
 81    dur.tv_nsec = 0;
 82    em          = amongoc_timeout(&loop, em, dur);
 83
 84    em = amongoc_let(em,
 85                     amongoc_async_forward_errors,
 86                     amongoc_box_pointer(&state),
 87                     after_connect_say_hello);
 88
 89    amongoc_operation op = amongoc_tie(em, &status);
 90    amongoc_start(&op);
 91    amongoc_default_loop_run(&loop);
 92    amongoc_operation_delete(op);
 93
 94    // Destroy the connection since we are done with it (this is a no-op for a null connection)
 95    amongoc_client_delete(state.client);
 96    amongoc_default_loop_destroy(&loop);
 97
 98    // Final status
 99    amongoc_if_error (status, msg) {
100        fprintf(stderr, "An error occurred: %s\n", msg);
101        return 2;
102    } else {
103        printf("Okay\n");
104        return 0;
105    }
106}
107// end.

Headers#

We first include the “everything” library header:

Header file inclusions#
1#include <amongoc/amongoc.h>

This will make all public APIs visible. Individual APIs can be included on a header-by-header basis. Refer to the documentation on a component to learn what header contains it.

Command-Line Arguments#

The first action we perform in main() is checking our arguments:

Argument checking#
62int main(int argc, char const* const* argv) {
63    if (argc != 2) {
64        fprintf(stderr, "Usage: %s <uri>\n", argv[0]);
65        return 1;
66    }
67    const char* const uri = argv[1];

We will use argv[1] as the connection URI string.

Initializing the Loop#

The first “interesting” code will declare and initialize the default event loop:

69    amongoc_loop   loop;
70    amongoc_status status = amongoc_default_loop_init(&loop);
71    amongoc_if_error (status, msg) {
72        fprintf(stderr, "Error setting up the event loop: %s\n", msg);
73        return 2;
74    }

Hint

It is possible to provide your own event loop implementation, but that is out of scope for this page.

Declare the App State#

We use a type app_state to store some state that is shared across the application:

12typedef struct app_state {
13    // The connection to a server
14    amongoc_client* client;
15} app_state;

This state needs to be stored in a way that it outlives the scope of each sub-operation in the program. For this reason, we declare the instance in main():

76    struct app_state state = {0};

This will zero-initialize the contents of the struct. This is important so that the declared amongoc_client object is also null, ensuring that the later call to amongoc_client_delete() will be a no-op if that field is never initialized.

We will pass the state around by-reference in a box using amongoc_box_pointer().

Create a Client with a Timeout#

We create a connect operation using amongoc_client_new(), and then attach a timeout using amongoc_timeout()

78    amongoc_emitter em = amongoc_client_new(&loop, uri);
79    struct timespec dur;
80    dur.tv_sec  = 5;
81    dur.tv_nsec = 0;
82    em          = amongoc_timeout(&loop, em, dur);

We pass the emitter em to amongoc_timeout() on the same line as as overwriting it with the return value. This emitter-exchange pattern is common and preferred for building composed asynchronous operations.

Attach the First Continuation#

84    em = amongoc_let(em,
85                     amongoc_async_forward_errors,
86                     amongoc_box_pointer(&state),
87                     after_connect_say_hello);

The amongoc_let() function will attach a continuation to an operation in a way that lets us initiate a subsequent operation. We use amongoc_async_forward_errors to tell amongoc_let() that it should call our continuation only if the input operation resolves with a non-error status. If amongoc_client_new() fails with an error (including a timeout), then after_connect_say_hello will not be called, and the errant result will simply be passed to the next step in the chain.

The First Continuation#

The first step, after connecting to a server, is after_connect_say_hello, a continuation function given to amongoc_let():

36/** after_connect_say_hello()
37 * @brief Handle the connection to a server. Sends a "hello" message
38 *
39 * @param state_ptr Pointer to the `app_state`
40 * @param cl_box An `amongoc_client*`
41 * @return amongoc_emitter
42 */
43amongoc_emitter after_connect_say_hello(amongoc_box state_ptr, amongoc_status, amongoc_box cl_box) {

The amongoc_status parameter is unused: Because we passed amongoc_async_forward_errors in amongoc_let(), our continuation will only be called if the amongoc_status would be zero, so there is no need to check it.

Capture the Client

Upon success, the operation from amongoc_client_new() will resolve with an amongoc_client in its boxed result value. We move the connection by-value from the box and store it in our application state:

43amongoc_emitter after_connect_say_hello(amongoc_box state_ptr, amongoc_status, amongoc_box cl_box) {
44    printf("Connected to server\n");
45    // Store the connection in our app state
46    amongoc_box_take(amongoc_box_cast(app_state*, state_ptr)->client, cl_box);

The amongoc_box_take() macro is used to transfer the ownership from a type-erased amongoc_box into a typed storage for the object that it holds. The object that was owned by the box is now owned by the storage destination.

Build and Prepare a Command

48    // Create a "hello" command
49    bson_doc doc = bson_new();
50    bson_mut mut = bson_mutate(&doc);
51    bson_insert(&mut, "hello", "1");
52    bson_insert(&mut, "$db", "test");
53    amongoc_emitter em = amongoc_client_command(amongoc_box_cast(app_state*, state_ptr)->client,
54                                                bson_view_from(mut));
55    bson_delete(doc);

We build a MongoDB command { hello: "1", $db: "test" } and prepare to send it as a command with amongoc_client_command(). We can delete the prepared BSON document here as a copy of the data is now stored within the prepared operation in em.

Attach the Second Continuation

57    em = amongoc_then(em, amongoc_async_forward_errors, state_ptr, after_hello);

We set the next step in the process to call after_hello once the command has completed. We use amongoc_then() instead of amongoc_let(), because after_hello does not return a new operation to continue, just a new result value.

The amongoc_async_forward_errors flag tells amongoc_then() to not call after_hello if the operation enounters an error, and will instead forward the error to the next operation in the chain.

The Second Continuation#

The second continuation after we receive a response from the server is very simple:

17/** after_hello()
18 * @brief Handle the `hello` response from the server
19 *
20 * @param state_ptr Pointer to the `app_state`
21 * @param resp_data A `bson_mut` object that contains the response message
22 * @return amongoc_box Returns `amongoc_nil`
23 */
24amongoc_box after_hello(amongoc_box state_ptr, amongoc_status*, amongoc_box resp_data) {
25    (void)state_ptr;
26    bson_view resp = bson_view_from(amongoc_box_cast(bson_doc, resp_data));
27    // Just print the response message
28    fprintf(stdout, "Got response: ");
29    bson_write_repr(stdout, resp);
30    fputs("\n", stdout);
31    amongoc_box_destroy(resp_data);
32    return amongoc_nil;
33}

We simply extract a bson_view of the response data from the resp_data box that was provided by amongoc_client_command() and print it to standard output. After we finish printing it, we destroy the data and return amongoc_nil. This is the final value for the program.

Tie the Final Result#

Going back to main(), after our call to amongoc_let() in which we attached the first continuation, we use amongoc_tie() to convert the emitter to an amongoc_operation:

89    amongoc_operation op = amongoc_tie(em, &status);

This will allow us to see the final result status of the program in fin_status after the returned operation op completes.

Start the Operation, Run the Loop, and Clean Up#

90    amongoc_start(&op);
91    amongoc_default_loop_run(&loop);
92    amongoc_operation_delete(op);
93
94    // Destroy the connection since we are done with it (this is a no-op for a null connection)
95    amongoc_client_delete(state.client);
96    amongoc_default_loop_destroy(&loop);

We now launch the composed operation using amongoc_start(). This will enqueue work on the event loop. We then give the event loop control to run the program using amongoc_default_loop_run(). After the event loop returns, all asynchronous work has completed, and we destroy the operation with amongoc_operation_delete(). This will free resources associated with the operation.

We also now have a copy of the connection that was created with amongoc_client_new(). We destroy that with amongoc_client_delete(). If the connect operation failed, the connection object will have remained zero-initialized and the call will be a no-op.

Finally, we are done with the event loop, and we can destroy it with amongoc_default_loop_destroy().