Connect to a Server#

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

connect.example.c#
  1#include <amongoc/amongoc.h>
  2
  3#include "bson/view.h"
  4#include <bson/iterator.h>
  5#include <bson/mut.h>
  6
  7#include "mlib/str.h"
  8
  9/**
 10 * @brief Shared state for the application. This is passed through the app as pointer stored
 11 * in a box
 12 */
 13typedef struct app_state {
 14    // The connection to a server
 15    amongoc_client* client;
 16} app_state;
 17
 18/**
 19 * @brief Write the content of a BSON document in JSON-like format to the given output
 20 */
 21static void print_bson(FILE* into, bson_view doc, mlib_str_view indent);
 22
 23/** after_hello()
 24 * @brief Handle the `hello` response from the server
 25 *
 26 * @param state_ptr Pointer to the `app_state`
 27 * @param resp_data A `bson_mut` object that contains the response message
 28 * @return amongoc_box Returns `amongoc_nil`
 29 */
 30amongoc_box after_hello(amongoc_box state_ptr, amongoc_status*, amongoc_box resp_data) {
 31    (void)state_ptr;
 32    bson_view resp = bson_view_from(amongoc_box_cast(bson_doc, resp_data));
 33    // Just print the response message
 34    fprintf(stdout, "Got response: ");
 35    print_bson(stdout, resp, mlib_str_view_from(""));
 36    fputs("\n", stdout);
 37    amongoc_box_destroy(resp_data);
 38    return amongoc_nil;
 39}
 40// end.
 41
 42/** after_connect_say_hello()
 43 * @brief Handle the connection to a server. Sends a "hello" message
 44 *
 45 * @param state_ptr Pointer to the `app_state`
 46 * @param cl_box An `amongoc_client*`
 47 * @return amongoc_emitter
 48 */
 49amongoc_emitter after_connect_say_hello(amongoc_box state_ptr, amongoc_status, amongoc_box cl_box) {
 50    printf("Connected to server\n");
 51    // Store the connection in our app state
 52    amongoc_box_take(amongoc_box_cast(app_state*, state_ptr)->client, cl_box);
 53
 54    // Create a "hello" command
 55    bson_doc doc = bson_new();
 56    bson_mut mut = bson_mutate(&doc);
 57    bson_insert(&mut, "hello", "1");
 58    bson_insert(&mut, "$db", "test");
 59    amongoc_emitter em = amongoc_client_command(amongoc_box_cast(app_state*, state_ptr)->client,
 60                                                bson_view_from(mut));
 61    bson_delete(doc);
 62
 63    em = amongoc_then(em,
 64                      amongoc_async_forward_errors,
 65                      mlib_default_allocator,
 66                      state_ptr,
 67                      after_hello);
 68    return em;
 69}
 70// end.
 71
 72int main(int argc, char const* const* argv) {
 73    if (argc != 2) {
 74        fprintf(stderr, "Usage: %s <uri>\n", argv[0]);
 75        return 1;
 76    }
 77    const char* const uri = argv[1];
 78
 79    amongoc_loop loop;
 80    amongoc_default_loop_init(&loop);
 81
 82    struct app_state state = {0};
 83
 84    amongoc_emitter em = amongoc_client_new(&loop, uri);
 85    struct timespec dur;
 86    dur.tv_sec  = 5;
 87    dur.tv_nsec = 0;
 88    em          = amongoc_timeout(&loop, em, dur);
 89
 90    em = amongoc_let(em,
 91                     amongoc_async_forward_errors,
 92                     mlib_default_allocator,
 93                     amongoc_box_pointer(&state),
 94                     after_connect_say_hello);
 95
 96    amongoc_status    fin_status = amongoc_okay;
 97    amongoc_operation op         = amongoc_tie(em, &fin_status, NULL, mlib_default_allocator);
 98    amongoc_start(&op);
 99    amongoc_default_loop_run(&loop);
100    amongoc_operation_delete(op);
101
102    // Destroy the connection since we are done with it (this is a no-op for a null connection)
103    amongoc_client_delete(state.client);
104    amongoc_default_loop_destroy(&loop);
105
106    if (amongoc_is_error(fin_status)) {
107        char* m = amongoc_status_strdup_message(fin_status);
108        fprintf(stderr, "An error occurred: %s\n", m);
109        free(m);
110        return 2;
111    } else {
112        printf("Okay\n");
113        return 0;
114    }
115}
116// end.
117
118static void print_bson(FILE* into, bson_view doc, mlib_str_view indent) {
119    fprintf(into, "{\n");
120    bson_foreach(it, doc) {
121        mlib_str_view str = bson_key(it);
122        fprintf(into, "%*s  \"%s\": ", (int)indent.len, indent.data, str.data);
123        bson_value_ref val = bson_iterator_value(it);
124        switch (val.type) {
125        case bson_type_eod:
126        case bson_type_double:
127            fprintf(into, "%f,\n", val.double_);
128            break;
129        case bson_type_utf8:
130            fprintf(into, "\"%s\",\n", val.utf8.data);
131            break;
132        case bson_type_document:
133        case bson_type_array: {
134            mlib_str  i2     = mlib_str_append(indent, "  ");
135            bson_view subdoc = bson_iterator_value(it).document;
136            print_bson(into, subdoc, mlib_str_view_from(i2));
137            mlib_str_delete(i2);
138            fprintf(into, ",\n");
139            break;
140        }
141        case bson_type_undefined:
142            fprintf(into, "[undefined],\n");
143            break;
144        case bson_type_bool:
145            fprintf(into, val.bool_ ? "true,\n" : "false,\n");
146            break;
147        case bson_type_null:
148            fprintf(into, "null,\n");
149            break;
150        case bson_type_int32:
151            fprintf(into, "%d,\n", val.int32);
152            break;
153        case bson_type_int64:
154            fprintf(into, "%ld,\n", val.int64);
155            break;
156        case bson_type_timestamp:
157        case bson_type_decimal128:
158        case bson_type_maxkey:
159        case bson_type_minkey:
160        case bson_type_oid:
161        case bson_type_binary:
162        case bson_type_datetime:
163        case bson_type_regex:
164        case bson_type_dbpointer:
165        case bson_type_code:
166        case bson_type_symbol:
167        case bson_type_codewscope:
168            fprintf(into, "[[printing unimplemented for this type]],\n");
169            break;
170        }
171    }
172    fprintf(into, "%*s}", (int)indent.len, indent.data);
173}

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#
72int main(int argc, char const* const* argv) {
73    if (argc != 2) {
74        fprintf(stderr, "Usage: %s <uri>\n", argv[0]);
75        return 1;
76    }
77    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:

79    amongoc_loop loop;
80    amongoc_default_loop_init(&loop);

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:

13typedef struct app_state {
14    // The connection to a server
15    amongoc_client* client;
16} 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():

82    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()

84    amongoc_emitter em = amongoc_client_new(&loop, uri);
85    struct timespec dur;
86    dur.tv_sec  = 5;
87    dur.tv_nsec = 0;
88    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#

90    em = amongoc_let(em,
91                     amongoc_async_forward_errors,
92                     mlib_default_allocator,
93                     amongoc_box_pointer(&state),
94                     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():

42/** after_connect_say_hello()
43 * @brief Handle the connection to a server. Sends a "hello" message
44 *
45 * @param state_ptr Pointer to the `app_state`
46 * @param cl_box An `amongoc_client*`
47 * @return amongoc_emitter
48 */
49amongoc_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:

49amongoc_emitter after_connect_say_hello(amongoc_box state_ptr, amongoc_status, amongoc_box cl_box) {
50    printf("Connected to server\n");
51    // Store the connection in our app state
52    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

54    // Create a "hello" command
55    bson_doc doc = bson_new();
56    bson_mut mut = bson_mutate(&doc);
57    bson_insert(&mut, "hello", "1");
58    bson_insert(&mut, "$db", "test");
59    amongoc_emitter em = amongoc_client_command(amongoc_box_cast(app_state*, state_ptr)->client,
60                                                bson_view_from(mut));
61    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

63    em = amongoc_then(em,
64                      amongoc_async_forward_errors,
65                      mlib_default_allocator,
66                      state_ptr,
67                      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:

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

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:

96    amongoc_status    fin_status = amongoc_okay;
97    amongoc_operation op         = amongoc_tie(em, &fin_status, NULL, mlib_default_allocator);

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).

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

 98    amongoc_start(&op);
 99    amongoc_default_loop_run(&loop);
100    amongoc_operation_delete(op);
101
102    // Destroy the connection since we are done with it (this is a no-op for a null connection)
103    amongoc_client_delete(state.client);
104    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().