Connect to a Server#
This how-to guide will walk through the follow example program:
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:
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:
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);
See also
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()
.
Print the Final Result#
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}
Finally, we inspect the amongoc_status
that was produced by our operation and
updated using amongoc_tie()
. This will tell us if there were any errors during
the execution of the program. If so, we print the error message to standard
error and exit non-zero from the program.