# `libac`: Acron Client Library A client library written in C, based on [json-c](https://github.com/json-c/json-c) and [wic](https://github.com/cjhdev/wic). ## Building Requirements: * `json-c` installed and using PkgConfig * Git * Meson * A C11 or higher C compiler * Connectivity to this git repository and `github.com` To build on Unix: ```shell git clone https://git.yuuta.moe/Minecraft/acron.git cd acron git submodule update --init cd client/libacron/ meson build -Dbuildtype=release cd build ninja DESTDIR=$(pwd)/install meson install ``` The shared library will be at `libac.so` (`ac.dll` on Windows). The static library will be at `libac-static.a` (`ac-static.lib` on Windows). The distributable headers are at `install/usr/local/include/libac/`. To make debug builds, use the Debug build type. Debug builds will have ASAN enabled. ## Usage All functions begin with `ac_`. Include `libac.h`. ### Thread Safety This library is single-threaded, meaning that all functions should be called on a dedicated thread. However, user may initialize the library on multiple threads and call the functions on different threads without interfering each other. ### Error Handling All error codes are at `common.h`. `AC_E_*` are library errors (generated by library routines); `AC_ER_*` are remote errors (generated by the server), and you should match them with `ac_response_error.code`. All libac functions return an integer value of the error code. `AC_E_OK(0)` indicates successful operation. Any library function calls not following the document will return `AC_E_INVALID_REQUEST`. ### Struct Inheritance All libac exported structs have a `uint8_t type` as its first member. It indicates the type of the object. The type ID is globally unique and stable across upgrades. Internally, the type is an unsigned 8-bit integer, with the first two bits indicating whether it is an event, a response, or a request. The remaining 6 bits differentiates the type with others. Library users usually do not need to care about the internal assignment of type IDs. Every exported struct have its ID defined in the corresponding headers (e.g. `AC_RESPONSE_OK`). Macros like `AC_IS_EVENT` is also available (from `ids.h`). Thus, the base definition of all libac structs is: ```c /* ids.h */ typedef struct ac_obj { uint8_t type; } ac_obj_t; ``` are classified in three types: * Event: Server-initiated events. Do not have IDs. Base definition: `ac_event_t`. ```c /* events.h */ typedef struct ac_event { uint8_t type; } ac_event_t; ``` * Request: Program-allocated requests. Have a program-assigned ID. Base definition: `ac_request_t`. ```c /* requests.h */ typedef struct ac_request { uint8_t type; int id; } ac_request_t; ``` * Response: Responses to requests. Have the same ID as the request. Base definition: `ac_response_t`. ```c /* requests.h */ typedef struct ac_response { uint8_t type; int id; } ac_response_t; ``` ### Struct Memory Management For requests only, it is the program's responsibility to allocate them (on whether stack or heap) and assign the correct type, so libac can recognize them. The program should also free the request after making `ac_request()` calls, as libac will not modify the request object: ```c int list(void *connection) { /* Allocated on the stack: Freed upon return, not by libac. */ ac_request_cmd cmd = { .type = AC_REQUEST_CMD, .id = 114514, .config = NULL, .cmd = "list" }; int r = ac_request(connection, (ac_request_t *) &cmd); /* cmd is freed. */ return r; } ``` For events and responses, libac will allocate them internally, and the program should free them using `ac_obj_free(ac_obj_t)` function. ### Initialization To initialize the library on the current thread, call: `ac_init()`: ```c int main(void) { libac_config_t config = { .out = NULL, /* Error output stream. NULL to disable any error logging. */ .tok = NULL } int r = ac_init(&config); if (r) return r; } ``` When finished using libac on the thread, call `ac_free()`. Any function call without `ac_init()` first will return `AC_E_NOT_INITIALIZED`. ### Making Connections To connect to a server on the current thread, use `ac_connect()`. It will output a `void *` opaque pointer referencing to the connection. Programs should pass this pointer as-is when making connections or disconnecting. Every thread can have multiple connections simultaneously. In addition, the client is required to provide its own socket IO implementations due to: 1. Cross-platform is easier. 2. It is easier for users to supply their own async IO mechanism. The client needs to establish a socket connection before calling ac_connect. It should also supply the pointer to its socket identifier (anything is accepted: libac only needs a pointer) to `ac_connection_parameters`. A typical type is `* int` (pointer to socket fd). The client also needs to supply its own `on_send` function. libac will call that function when it needs to write anything to the socket. When calling that function, the socket pointer will be passed as-is. No worries, `apps/helloworld/net.c` provides a ready-to-use example of cross-platform (Unix and Windows) blocking socket client. See `apps/helloworld/main.c` for more details. ```c void *connection = NULL; int r; ac_connection_parameters_t args = { .host = "127.0.0.1", .port = 25575, .version = 0, .id = "1", .token = "123", .on_send = /* TODO: on_send callback function */, .socket = NULL }; int socket = /* TODO: Connect to the socket. */; args.socket = &socket; if ((r = ac_connect(args, &connection))) { return r; } /* Listen or make requests. */ ``` Upon `ac_connect`, libac only does a little to initialize the connection (e. g. write initial handshake request to the socket). The handshake is not yet completed, and the client cannot send requests yet. To display the state, libac provides a function called `ac_get_state()` which outputs the current state to the connection (`AC_STATE_INIT`, `AC_STATE_READY` or `AC_STATE_CLOSED`). A common usage is to loop `ac_receive` (see below) infinitely after `ac_connect` until the state changes from `AC_STATE_INIT` to `AC_STATE_READY`, where `ac_request` is allowd and valid data will return from `ac_receive`. Note: if the state changed to `AC_STATE_CLOSED` within `ac_receive`, this is likely due to an abnormal termination of the connection. In this case, `ac_receive` will return `AC_E_NET`. Then, the program can listen for responses or events. The client should do its own magic to receive from socket and supply `ac_receive` with a buffer and size. Function `ac_receive` may only parse part of the buffer, so it is the client's responsibility to keep track of the bytes read: ```c ac_obj_t *obj = NULL; uint8_t buffer[1000U]; size_t len; size_t pos = 0; size_t read; /* TODO: recv(): reset pos to 0 and set len. */ while (1) { if ((r = ac_receive(connection, buffer, pos, len, &obj, &read))) { /* Handle error. */ break; } pos += read; /* The obj is now referencing to a library allocated event or response. */ /* Do things with the event or response. */ ac_obj_free(obj); } ``` > **Note**: > > In some cases, the buffer may contain more than one valid WebSocket frames. In such cases, > `ac_receive` will backlog the additional parsed messages, and they will return the second time > calling `ac_receive` with buffer = bytes = 0. > > This will happen only `ac_receive` returns `AC_E_AGAIN`. If that is true, the client must additionally > call `ac_receive` infinite times with buffer = bytes = 0, until either `ac_receive` returns an error or > returns `AC_E_OK`. See the code below: ```c ac_obj_t *obj = NULL; uint8_t buffer[1000U]; size_t len; size_t pos = 0; size_t read; /* TODO: recv(): reset pos to 0 and set len. */ bool again = false; while (1) { if ((r = ac_receive(connection, buffer, pos, len, &obj, &read))) { if (r == AC_E_AGAIN) { again = true; } else { /* Handle error. */ break; } } pos += read; /* The obj is now referencing to a library allocated event or response. */ /* Do things with the event or response. */ ac_obj_free(obj); /* Clear backlog */ while (again) { if ((r = ac_receive(connection, NULL, 0, 0, &obj, NULL))) { if (r == AC_E_AGAIN) { again = true; } else { /* Handle error. */ break; } } ac_obj_free(obj); } } ``` The program can make requests using `ac_request()`: ```c ac_config_t conf = { .name = "helloworld" }; ac_request_cmd_t req = { .type = AC_REQUEST_CMD, .id = 100, .config = &conf, .cmd = "say Hi" }; ac_request(connection, (ac_request_t *) &req)); ``` Finally, when done, call `ac_disconnect()`. It will close the socket and free the `connection` parameter. The `connection` pointer after the method call is undefined. The client must manually close its socket after calling `ac_disconnect()`. > **Notes**: > > Only unencrypted WebSocket is supported at this time. `wss://` support is > on the roadmap. Read more: `apps/helloworld/main.c` example client. #### Connection Threading libac reads stores some state in the connection object upon each function call. Therefore, it is the caller's responsibility to guard the whole connection with a mutex. libac does not provide that because this may limit the flexibility. After locking, it is safe to call the connection-related functions from any threads. Example `apps/helloworld/main.c` has a naive cross-platform example of how to guard the connection using a mutex. ## Development To make development builds, use the following `meson(1)` command: ```shell CC=cc meson build -Dsanitize=address ``` 1. If you have `ccache` installed, you need to manually specify a C compiler to NOT use ccache, as CLion does not recognize ccache. 2. Use `-Dsanitize` to enable ASAN. To open in CLion, refer to [https://blog.jetbrains.com/clion/2021/01/working-with-meson-in-clion-using-compilation-db](https://blog.jetbrains.com/clion/2021/01/working-with-meson-in-clion-using-compilation-db/). Be warned that CLion support is still primitive. ## Roadmap * Make unit tests * SSL support * Calling `ac_request()` from any thread ## License The libac library is licensed under LGPL-2.1-only. Other parts of Acron is still licensed under GPL-2.0-only.