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:
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:
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 }
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:
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()
.
Print the Final Result#
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}
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.