Client API
You can use C API client library to connect to Gliimly:
- The API has only a few functions, and the main one is "gg_cli_request()", which makes a call to the service.
- There is only a single data type used, which is "gg_cli" and it is used to specify a request and its options, as well as to retrieve results.
- There is a single include file ("gcli.h").
- When building your client executable, you can either specify build flags by using the result of "gg -i" (if you have Gliimly installed), or use self-contained source files directly.
- It is MT-safe, so you can use it in multi-threaded applications, such as to make many requests in parallel.
See Examples section below for detailed examples.
Sending a request to Gliimly service
The following function is used to make a call using C API:
int gg_cli_request (gg_cli *req);
Copied!
All input and output is contained in a single variable of type "gg_cli", the pointer to which is passed to "gg_cli_request()" function that sends a request to the service. A variable of type "gg_cli" must be initialized to zero before using it (such as with {0} initialization, "memset()" or "calloc()"), or otherwise some of its members may have random values:
gg_cli req = {0};
0
...
...
int result = gg_cli_request (&req);
Copied!
Type "gg_cli" is defined as (i.e. public members of it):
typedef struct {
const char *server;
const char *req_method;
const char *app_path;
const char *req;
const char *url_params;
const char *content_type;
int content_len;
const char *req_body;
char **env;
int timeout;
int req_status;
int data_len;
int error_len;
char *errm;
gg_cli_out_hook out_hook;
gg_cli_err_hook err_hook;
gg_cli_done_hook done_hook;
int thread_id;
volatile char done;
int return_code;
} gg_cli;
Copied!
- Mandatory input
The following members of "gg_cli" type must be supplied in order to make a call to a service:
- "server" represents either a Unix socket or a TCP socket, and is:
- for a Unix socket, a fully qualified name to a Unix socket file used to communicate with the service (for a Gliimly service, it's "/var/lib/gg/<app name>/sock/sock", where <app name> is the application name), or
- for a TCP socket, a host name and port name in the form of "<host name>:<port number>", specifying where is the server listening on (for instance "127.0.0.1:2301" if the Gliimly service is local and runs on TCP port 2301).
- "req_method" is a request method, such as "GET", "POST", "PUT", "DELETE" or any other.
- "app_path" is an application path (see request). By default it's "/<application name>".
- "req" is a request path, i.e. a request name preceded by a forward slash, as in "/<request name>" (see request).
- URL parameters
"url_params" is the URL parameters, meaning input parameters (as path segments and query string, see request). URL parameters can be NULL or empty, in which case it is not used.
- Request body (content)
"req_body" is the request body, which can be any text or binary data. "content_type" is the content type of request body (for instance "application/json" or "image/jpg"). "content_len" is the length of request body in bytes. A request body is sent only if "content_type" and "req_body" are not NULL and not empty, and if "content_len" is greater than zero.
- Passing environment to service
"env" is any environment variables that should be passed along to the service. You can access those in Gliimly via "environment" clause of get-sys statement. This is an array of strings, where name/value pairs are specified one after the other, and which always must end with NULL. For example, if you want to use variable "REMOTE_USER" with value "John" and variable "MY_VARIABLE" with value "8000", then it might look like this:
char *env[5];
env[0] = "REMOTE_USER";
env[1] = "John"
env[2] = "MY_VARIABLE";
env[3] = "8000"
env[4] = NULL;
Copied!
Thus, if you are passing N environment variables to the service, you must size "env" as "char*" array with 2*N+1 elements.
Note that in order to suppress output of HTTP headers from the service, you can include environment variable "GG_SILENT_HEADER" with value "yes"; to let the service control headers output (either by default, with "-z" option of mgrg or with silent-header) simply omit this environment variable.
- Timeout
"timeout" is the number of seconds a call to the service should not exceed. For instance if the remote service is taking too long or if the network connection is too slow, you can limit how long to wait for a reply. If there is no timeout, then "timeout" value should be zero. Note that DNS resolution of the host name (in case you are using a TCP socket) is not counted in timeout. Maximum value for timeout is 86400.
Even if timeout is set to 0, a service call may eventually timeout due to underlying socket and network settings. Note that even if your service call times out, the actual service executing may continue until it's done.
- Thread ID
"thread_id" is an integer that you can set and use when your program is multithreaded. By default it's 0. This number is set by you and passed to hooks (your functions called when request is complete or data available). You can use this number to differentiate the data with regards to which thread it belongs to.
- Completion indicator and return code
When your program is multithreaded, it may be useful to know when (and if) a request has completed. "done" is set to to "true" when a request completes, and "return_code" is the return value from gg_cli_request() (see below for a list). In a single-threaded program, this information is self-evident, but if you are running more than one request at the same time (in different threads), you can use these to check on each request executing in parallel (for instance in a loop in the main thread).
Note that "done" is "true" specifically when all the results of a request are available and the request is about to be completed. In a multithreaded program, it means the thread is very soon to terminate or has already terminated; it does not mean that thread has positively terminated. Use standard "pthread_join()" function to make sure the thread has terminated if that is important to you.
Return value of gg_cli_request()
The following are possible return values from "gg_cli_request()" (available in "return_code" member of "gg_cli" type):
- GG_OKAY if request succeeded,
- GG_CLI_ERR_RESOLVE_ADDR if host name for TCP connection cannot be resolved,
- GG_CLI_ERR_PATH_TOO_LONG if path name of Unix socket is too long,
- GG_CLI_ERR_SOCKET if cannot create a socket (for instance they are exhausted for the process or system),
- GG_CLI_ERR_CONNECT if cannot connect to server (TCP or Unix alike),
- GG_CLI_ERR_SOCK_WRITE if cannot write data to server (for instance if server has encountered an error or is down, or if network connection is no longer available),
- GG_CLI_ERR_SOCK_READ if cannot read data from server (for instance if server has encountered an error or is down, or if network connection is no longer available),
- GG_CLI_ERR_PROT_ERR if there is a protocol error, which indicates a protocol issue on either or both sides,
- GG_CLI_ERR_BAD_VER if either side does not support protocol used by the other,
- GG_CLI_ERR_SRV if server cannot complete the request,
- GG_CLI_ERR_UNK if server does not recognize record types used by the client,
- GG_CLI_ERR_OUT_MEM if client is out of memory,
- GG_CLI_ERR_ENV_TOO_LONG if the combined length of all environment variables is too long,
- GG_CLI_ERR_ENV_ODD if the number of supplied environment name/value pairs is incorrect,
- GG_CLI_ERR_BAD_TIMEOUT if the value for timeout is incorrect,
- GG_CLI_ERR_TIMEOUT if the request timed out based on "timeout" parameter or otherwise if the underlying Operating System libraries declared their own timeout.
You can obtain the error message (corresponding to the above return values) in "errm" member of "gg_cli" type.
The service reply is split in two. One part is the actual result of processing (called "stdout" or standard output), and that is "data". The other is the error messages (called "stderr" or standard error), and that's "error". All of service output goes to "data", except from report-error and pf-out/pf-url/pf-web (with "to-error" clause) which goes to "error". Note that "data" and "error" streams can be co-mingled when output by the service, but they will be obtained separately. This allows for clean separation of output from any error messages.
You can obtain service reply when it's ready in its entirety (likely most often used), or as it comes alone bit by bit (see more about asynchronous hooks futher here).
Status of request execution
"req_status" member of "gg_cli" type is the request status when a request had executed; it is somewhat similar to an exit status of a program. A Gliimly service request returns status by means of handler-status statement. Note that "req_status" is valid only if "gg_cli_request()" returned GG_OKAY (or if "return_code" is GG_OKAY for multi-threaded programs).
Getting data reply (stdout)
Data returned from a request is valid only if "gg_cli_request()" returned GG_OKAY (or if "return_code" is GG_OKAY for multi-threaded programs). In that case, use "gg_cli_data()" function, for example:
gg_cli req = {0};
...
if (gg_cli_request (&req) == GG_OKAY) {
char *data = gg_cli_data (req);
int data_len = req->data_len;
}
Copied!
"data_len" member of "gg_cli" type will have the length of data response in bytes. The reply is always null-terminated as a courtesy, and "data_len" does not include the terminating null byte.
"gg_cli_data()" returns the actual response (i.e. data output) from service as passed to "data" stream. Any output from service will go there, except when "to-error" clause is used in pf-out, pf-url and pf-web - use these constructs to output errors without stopping the service execution. Additionaly, the output of report-error will also not go to data output.
Getting error reply (stderr)
An error reply returned from a service is valid only if "gg_cli_request()" returned GG_OKAY (or if "return_code" is GG_OKAY for multi-threaded programs). In that case, use "gg_cli_error()" function, for example:
gg_cli req = {0};
...
if (gg_cli_request (&req) == GG_OKAY) {
char *err = gg_cli_error (req);
int err_len = req->error_len;
}
Copied!
"gg_cli_error()" returns any error messages from a service response, i.e. data passed to "error" stream. It is comprised of any service output when "to-error" clause is used in pf-out, pf-url and pf-web, as well as any output from report-error.
"error_len" member (of "gg_cli" type above) will have the length of error response in bytes. The response is always null-terminated as a courtesy, and "error_len" does not include the terminating null byte.
Freeing the result of a request
Once you have obtained the result of a request, and when no longer needed, you should free it by using "gg_cli_delete()":
gg_cli req = {0};
...
gg_cli_request (&req);
gg_cli_delete (&req);
Copied!
If you do not free the result, your program may experience a memory leak. If your program exits right after issuing any request(s), you may skip freeing results as that is automatically done on exit by the Operating System.
You can use "gg_cli_delete()" regardless of whether "gg_cli_request()" returned GG_OKAY or not.
A function you wrote can be called when a request has completed. This is useful in multithreaded invocations, where you may want to receive complete request's results as they are available. To specify a completion hook, you must write a C function with the following signature and assign it to "done_hook" member of "gg_cli" typed variable:
typedef void (*gg_cli_done_hook)(char *recv, int recv_len, char *err, int err_len, gg_cli *req);
Copied!
"recv" is the request's data output, "recv_len" is its length in bytes, "err" is the request's error output, and "err_len" is its length in bytes. "req" is the request itself which you can use to obtain any other information about the request. In a single threaded environment, these are available as members of the request variable of "gg_cli" type used in the request, and there is not much use for a completion hook.
See an example with asynchronous hooks.
You can obtain the service's reply as it arrives by specifying read hooks. This is useful if the service supplies partial replies over a period of time, and your application can get those partial replies as they become available.
To specify a hook for data output (i.e. from stdout), you must write a C function with the following signature and assign it to "out_hook":
typedef void (*gg_cli_out_hook)(char *recv, int recv_len, gg_cli *req);
Copied!
"recv" is the data received and "recv_len" is its length.
To specify a hook for error output (i.e. from stderr), you must write a C function with the following signature and assign it to "err_hook":
typedef void (*gg_cli_err_hook)(char *err, int err_len, gg_cli *req);
Copied!
"err" is the error received and "err_len" is its length.
"req" (in both hooks) is the request itself which you can use to obtain any other information about the request.
To register these functions with "gg_cli_request()" function, assign their pointers to "out_hook" and "err_hook" members of request variable of type "gg_cli" respectively. Note that the output hook (i.e. hook function of type "gg_cli_out_hook") will receive empty string ("") in "recv" and "recv_len" will be 0 when the request has completed, meaning all service output (including error) has been received.
For example, functions "get_output()" and "get_err()" will capture data as it arrives and print it out, and get_complete() will print the final result:
void get_output(char *d, int l, gg_cli *req)
{
printf("Got output of [%.*s] of length [%d] in thread [%d]", l, d, l, req->thread_id);
}
void get_err(char *d, int l, gg_cli *req)
{
printf("Got error of [%.*s] of length [%d], status [%d]", l, d, l, req->req_status);
}
void get_complete(char *data, int data_len, char *err, int err_len, gg_cli *req)
{
printf("Got data [%.*s] of length [%d] and error of [%.*s] of length [%d], status [%d], thread [%d]\n", data_len, data, data_len, err_len, err, err_len, req->req_status, req->thread_id);
}
...
gg_cli req = {0};
...
req.out_hook = &get_output;
req.err_hook = &get_err;
req.done_hook = &get_complete;
Copied!
The Gliimly client is MT-safe, meaning you can use it both in single-threaded and multi-threaded programs. Note that each thread must have its own copy of "gg_cli" request variable, since it provides both input and output parameters to a request call and as such cannot be shared between the threads.
Do not use this API directly with Gliimly - use call-remote instead which is made specifically for use in .gliim files. Otherwise, you can use this API with any program.
Using API without Gliimly
You can use API without installing Gliimly. To do that:
- get the Gliimly source code,
- copy source files "gcli.c" and "gcli.h" to where you need to build your program,
- add the content of Gliimly's "LICENSE" file to your own in order to include the license for these source files,
- then make your application:
gcc -o cli cli.c gcli.c
Copied!
Note that you do not need to install any other dependencies, as API is entirely contained in the aforementioned source files.
The following example is a simple demonstration, with minimum of options used. Copy the C code to file "cli.c" in a directory of its own:
#include "gcli.h"
void main ()
{
gg_cli req = {0};
req.server = "/var/lib/gg/helloworld/sock/sock";
req.req_method = "GET";
req.app_path = "/helloworld";
req.req = "/hello-simple";
int res = gg_cli_request (&req);
if (res != GG_OKAY) printf("Request failed [%d] [%s]\n", res, req.errm);
else printf("%s", gg_cli_data(&req));
gg_cli_delete(&req);
}
Copied!
To make this client application:
gcc -o cli cli.c $(gg -i)
Copied!
In this case, you're using a Unix socket to communicate with the Gliimly service. To test with a Gliimly service handler, copy the following code to "hello_simple.gliim" file in a separate directory:
begin-handler /hello_simple public
silent-header
@Hi there!
end-handler
Copied!
Create and make the Gliimly application and run it via local Unix socket:
sudo mgrg -i -u $(whoami) helloworld
gg -q
mgrg -m quit helloworld
mgrg -w 1 helloworld
Copied!
Run the client:
./cli
Copied!
The output is, as expected:
Hi there!
Copied!
Example with more options
This example demonstrates using multiple options, including using TCP sockets connecting to a host and port number, environment variables, query string, request body and request execution timeout. It will also show the separation of "data" and "error" (i.e. stdout and stderr) streams from the service.
Copy this to file "cli1.c" in a directory of its own - note that in this example a server will run on localhost (127.0.0.1) and TCP port 2301:
#include "gcli.h"
void main ()
{
gg_cli req;
memset (&req, 0, sizeof(req));
3
char *env[] = { "REMOTE_USER", "John", "SOME_VAR", "SOME\nVALUE", "NEW_VAR", "1000", NULL };
req.env = env;
req.server = "127.0.0.1:2301";
req.req_method = "GET";
req.app_path = "/helloworld";
req.req = "/hello";
req.url_params = "par1=val1&par2=91";
req.content_type = "application/json";
req.req_body = "This is request body";
req.content_len = strlen (req.req_body);
0
req.timeout = 0;
int res = gg_cli_request (&req);
if (res != GG_OKAY) printf("Request failed [%d] [%s]\n", res, req.errm);
else {
printf("Server status %d\n", req.req_status);
printf("Len of data %d\n", req.data_len);
printf("Len of error %d\n", req.error_len);
printf("Data [%s]\n", gg_cli_data(&req));
printf("Error [%s]\n", gg_cli_error(&req));
}
gg_cli_delete(&req);
}
Copied!
Note that the URL parameters (i.e. "req.url_params") could have been written as a combination of a path segment and query string (see request):
req.url_params = "/par1/val1?par2=91";
Copied!
or just as a path segment:
req.url_params = "/par1=val1/par2=91";
Copied!
To make this client application:
gcc -o cli1 cli1.c $(gg -i)
Copied!
To test it, you can create a Gliimly application. Copy this to "hello.gliim" file in a separate directory:
begin-handler /hello public
silent-header
request-body rb
get-param par1
get-param par2
get-sys environment "REMOTE_USER" to ruser
get-sys environment "SOME_VAR" to somev
get-sys environment "NEW_VAR" to newv
get-req process-id to pid
@Hello World! [<<p-out ruser>>] [<<p-out somev>>] [<<p-out newv>>] [<<p-out par1>>] [<<p-out par2>>] <<p-num pid>> <<p-out rb>>
"Output line #<num of line>"
1418"Line 1419 has an error"
4418
start-loop repeat 8000 use i start-with 0
@Output line #<<p-num i>>
if-true i equal 1419
pf-out "Line %ld has an error\n", i to-error
end-if
if-true i equal 4419
handler-status 82
report-error "%s", "Some error!"
end-if
end-loop
end-handler
Copied!
Create and make the Gliimly application and run it on local TCP port 2301 to match the client above:
sudo mgrg -i -u $(whoami) helloworld
gg -q
mgrg -m quit helloworld
mgrg -w 1 -p 2301 helloworld
Copied!
Run the client:
./cli1
Copied!
The output:
Server status 82
Len of data 78530
Len of error 35
Data [Hello World! [John] [SOME
VALUE] [1000] [val1] [91] 263002 This is request body
Output line #0
Output line #1
Output line #2
Output line #3
Output line #4
Output line #5
Output line #6
Output line #7
...
Output line #4413
Output line #4414
Output line #4415
Output line #4416
Output line #4417
Output line #4418
Output line #4419
]
Error [Line 1419 has an error
Some error!
]
Copied!
The output shows service exit code (82, see handler-status in the Gliimly code above), length of data output, and other information which includes environment variables passed to the service from the client, the PID of server process, the request body from the client, and then the error output. Note that the data output (stdout) and the error output (stderr) are separated, since the protocol does use separate streams over the same connection. This makes working with the output easy, while the data transfer is fast at the same time.
API
Client-API
Server-API
See all
documentation
Copyright (c) 2019-2024 Gliim LLC. All contents on this web site is "AS IS" without warranties or guarantees of any kind.