{"id":15048904,"url":"https://github.com/smithsonian/smax-clib","last_synced_at":"2025-10-13T13:07:44.143Z","repository":{"id":257110556,"uuid":"804754173","full_name":"Smithsonian/smax-clib","owner":"Smithsonian","description":"C/C++ client library and toolkit for SMA-X","archived":false,"fork":false,"pushed_at":"2025-09-15T18:41:08.000Z","size":1876,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-10-13T13:06:34.800Z","etag":null,"topics":["c-language","c-programming-language","data-exchange","distributed-systems","library","open-source","real-time-database","real-time-systems","realtime-database","redis","structured-data"],"latest_commit_sha":null,"homepage":"https://smithsonian.github.io/smax-clib/","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Smithsonian.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"attipaci","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2024-05-23T07:49:37.000Z","updated_at":"2025-09-15T18:41:12.000Z","dependencies_parsed_at":"2024-11-12T20:22:07.542Z","dependency_job_id":"566df9d8-0d48-4818-9482-9e23a5db3271","html_url":"https://github.com/Smithsonian/smax-clib","commit_stats":{"total_commits":63,"total_committers":1,"mean_commits":63.0,"dds":0.0,"last_synced_commit":"12544197c08e774a2256c1870d942a5b33d54d23"},"previous_names":["smithsonian/smax-clib"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Smithsonian/smax-clib","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smithsonian%2Fsmax-clib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smithsonian%2Fsmax-clib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smithsonian%2Fsmax-clib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smithsonian%2Fsmax-clib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Smithsonian","download_url":"https://codeload.github.com/Smithsonian/smax-clib/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Smithsonian%2Fsmax-clib/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279015278,"owners_count":26085683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c-language","c-programming-language","data-exchange","distributed-systems","library","open-source","real-time-database","real-time-systems","realtime-database","redis","structured-data"],"created_at":"2024-09-24T21:17:04.413Z","updated_at":"2025-10-13T13:07:44.135Z","avatar_url":"https://github.com/Smithsonian.png","language":"C","funding_links":["https://github.com/sponsors/attipaci"],"categories":[],"sub_categories":[],"readme":"![Build Status](https://github.com/Smithsonian/smax-clib/actions/workflows/build.yml/badge.svg)\n![Test Status](https://github.com/Smithsonian/redisx/actions/workflows/test.yml/badge.svg)\n![Static Analysis](https://github.com/Smithsonian/smax-clib/actions/workflows/analyze.yml/badge.svg)\n\u003ca href=\"https://smithsonian.github.io/smax-clib/apidoc/html/files.html\"\u003e\n ![API documentation](https://github.com/Smithsonian/smax-clib/actions/workflows/dox.yml/badge.svg)\n\u003c/a\u003e\n\u003ca href=\"https://smithsonian.github.io/smax-clib/index.html\"\u003e\n ![Project page](https://github.com/Smithsonian/smax-clib/actions/workflows/pages/pages-build-deployment/badge.svg)\n\u003c/a\u003e\n\n\u003cpicture\u003e\n  \u003csource srcset=\"resources/CfA-logo-dark.png\" alt=\"CfA logo\" media=\"(prefers-color-scheme: dark)\"/\u003e\n  \u003csource srcset=\"resources/CfA-logo.png\" alt=\"CfA logo\" media=\"(prefers-color-scheme: light)\"/\u003e\n  \u003cimg src=\"resources/CfA-logo.png\" alt=\"CfA logo\" width=\"400\" height=\"67\" align=\"right\"/\u003e\n\u003c/picture\u003e\n\u003cbr clear=\"all\"\u003e\n\n# smax-clib\n\nA free C/C++ client library and toolkit for the \n[SMA Exchange (SMA-X)](https://docs.google.com/document/d/1eYbWDClKkV7JnJxv4MxuNBNV47dFXuUWu7C4Ve_YTf0/edit?usp=sharing) \nstructured real-time database\n\n - [API documentation](https://smithsonian.github.io/smax-clib/apidoc/html/files.html)\n - [Project pages](https://smithsonian.github.io/smax-clib) on github.io\n\nAuthor: Attila Kovacs\n\nUpdated for version 1.0 and later releases.\n\n## Citation\n\nIf you use SMA-X for your project, please cite as: \n\n - Kovács, Attila, Grimes, Paul K., Moriarty, Christopher, and Wilson, Robert, Journal of Astronomical Telescopes, Instruments, and Systems, Volume 11, id. 017001 (2025). (DOI: [10.1117/1.JATIS.11.1.017001](https://ui.adsabs.harvard.edu/link_gateway/2025JATIS..11a7001K/doi:10.1117/1.JATIS.11.1.017001)). \n\nYou may also find the citation record on ADS as [2025JATIS..11a7001K](https://ui.adsabs.harvard.edu/abs/2025JATIS..11a7001K/abstract).\n\n## Table of Contents\n\n - [Introduction](#smax-introduction)\n - [Prerequisites](#smax-prerequisites)\n - [Building the SMA-X C library](#building-smax)\n - [Linking your application against `smax-clib`](#smax-linking)\n - [Command-line tools](#command-line-tools)\n - [Initial configuration](#smax-configuration)\n - [Connecting to / disconnecting from SMA-X](#smax-connecting)\n - [Sharing and pulling data](#sharing-and-pulling)\n - [Lazy pulling (high-frequency queries)](#lazy-pulling)\n - [Pipelined pulls (high volume queries)](#pipelined-pulls)\n - [Custom update handling](#update-handling)\n - [Remote program control via SMA-X](#remote-control)\n - [Program status / error messages via SMA-X](#status-messages)\n - [Optional metadata](#optional-metadata)\n - [Error handling](#smax-error-handling)\n - [Debug support](#smax-debug-support)\n - [Future plans](#smax-future-plans)\n - [Release schedule](#smaxlib-release-schedule)\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"smax-introduction\"\u003e\u003c/a\u003e\n## Introduction\n\nThe [SMA Exchange (SMA-X)](https://docs.google.com/document/d/1eYbWDClKkV7JnJxv4MxuNBNV47dFXuUWu7C4Ve_YTf0/edit?usp=sharing) \nis a free, high performance, and versatile real-time data sharing platform for distributed software systems. It is \nbuilt around a central Redis (or equivalent) database, and provides efficient atomic access to structured data, \nincluding specific branches and/or leaf nodes, with associated metadata. SMA-X was developed at the Submillimeter \nArray (SMA) observatory, where we use it to share real-time data among hundreds of computers and nearly a thousand \nindividual programs. The __smax-clib__ library is free to use, in any way you like, without licensing restrictions.\n\nSMA-X consists of a set of server-side [LUA](https://lua.org/) scripts that run on [Redis](https://redis.io) (or one \nof its forks / clones such as [Valkey](https://valkey.io) or [Dragonfly](https://dragonfly.io)); a set of libraries to \ninterface client applications; and a set of command-line tools built with them. Currently we provide client libraries \nfor C/C++ (C99) and Python 3. We may provide Java and/or Rust client libraries too in the future. \n\nThere are no official releases of __smax-clib__ yet. An initial 1.0.0 release is expected early/mid 2025. \nBefore then the API may undergo slight changes and tweaks. Use the repository as is at your own risk for now.\n\n### Related links\n\n - [SMA-X specification](https://docs.google.com/document/d/1eYbWDClKkV7JnJxv4MxuNBNV47dFXuUWu7C4Ve_YTf0/edit?usp=sharing)\n - [Smithsonian/smax-python](https://github.com/Smithsonian/smax-python) an alternative library for Python 3.\n - [Smithsonian/smax-postgres](https://github.com/Smithsonian/smax-postgres) for creating a time-series history of \n   SMA-X in a __PostgreSQL__ database.\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"smax-prerequisites\"\u003e\u003c/a\u003e\n## Prerequisites\n\nThe SMA-X C/C++ library has a build and runtime dependency on the __xchange__ and __RedisX__ libraries also available\nat the Smithsonian Github repositories:\n\n - [Smithsonian/xchange](https://github.com/Smithsonian/xchange)\n - [Smithsonian/redisx](https://github.com/Smithsonian/redisx)\n\nAdditionally, to configure your Redis (or Valkey / Dragonfly) servers for SMA-X, you will need the \n[Smithsonian/smax-server](https://github.com/Smithsonian/smax-server) repo also.\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"building-smax\"\u003e\u003c/a\u003e\n## Building the SMA-X C library\n\nThe __smax-clib__ library can be built either as a shared (`libsmax.so[.1]`) and as a static (`libsmax.a`) library, \ndepending on what suits your needs best.\n\nYou can configure the build, either by editing `config.mk` or else by defining the relevant environment variables \nprior to invoking `make`. The following build variables can be configured:\n   \n - `CC`: The C compiler to use (default: `gcc`).\n\n - `CPPFLAGS`: C preprocessor flags, such as externally defined compiler constants.\n \n - `CFLAGS`: Flags to pass onto the C compiler (default: `-g -Os -Wall`). Note, `-Iinclude` will be added \n   automatically.\n \n - `CSTANDARD`: Optionally, specify the C standard to compile for, e.g. `c99` to compile for the C99 standard. If\n   defined then `-std=$(CSTANDARD)` is added to `CFLAGS` automatically.\n   \n - `WEXTRA`: If set to 1, `-Wextra` is added to `CFLAGS` automatically.\n\n - `FORTIFY`: If set it will set the `_FORTIFY_SOURCE` macro to the specified value (`gcc` supports values 1 \n   through 3). It affords varying levels of extra compile time / runtime checks.\n\n - `LDFLAGS`: Extra linker flags (default: _not set_). Note, `-lm  -lpthread -lredisx -lxchange` will be added \n   automatically.\n\n - `CHECKEXTRA`: Extra options to pass to `cppcheck` for the `make check` target\n\n - `DOXYGEN`: Specify the `doxygen` executable to use for generating documentation. If not set (default), `make` will\n   use `doxygen` in your `PATH` (if any). You can also set it to `none` to disable document generation and the\n   checking for a usable `doxygen` version entirely.\n\n - `XCHANGE`: If the [Smithsonian/xchange](https://github.com/Smithsonian/xchange) library is not installed on your\n   system (e.g. under `/usr`) set `XCHANGE` to where the distribution can be found. The build will expect to find \n   `xchange.h` under `$(XCHANGE)/include` and `libxchange.so` / `libxchange.a` under `$(XCHANGE)/lib` or else in the \n   default `LD_LIBRARY_PATH`.\n   \n - `REDISX`: If the [Smithsonian/redisx](https://github.com/Smithsonian/redisx) library is not installed on your\n   system (e.g. under `/usr`) set `REDISX` to where the distribution can be found. The build will expect to find \n   `redisx.h` under `$(REDISX)/include` and `libredisx.so` / `libredisx.a` under `$(REDISX)/lib` or else in the \n   default `LD_LIBRARY_PATH`.\n   \n - `STATICLINK`: Set to 1 to prefer linking tools statically against `libsmax.a`. (It may still link dynamically if \n   `libsmax.so` is also available.\n \nAfter configuring, you can simply run `make`, which will build the `shared` (`lib/libsmax.so[.1]`) and `static` \n(`lib/libsmax.a`) libraries, local HTML documentation (provided `doxygen` is available), and performs static\nanalysis via the `check` target. Or, you may build just the components you are interested in, by specifying the\ndesired `make` target(s). (You can use `make help` to get a summary of the available `make` targets). \n\nAfter building the library you can install the above components to the desired locations on your system. For a \nsystem-wide install you may simply run:\n\n```bash\n  $ sudo make install\n```\n\nOr, to install in some other locations, you may set a prefix and/or `DESTDIR`. For example, to install under `/opt` \ninstead, you can:\n\n```bash\n  $ sudo make prefix=\"/opt\" install\n```\n\nOr, to stage the installation (to `/usr`) under a 'build root':\n\n```bash\n  $ make DESTDIR=\"/tmp/stage\" install\n```\n\n-----------------------------------------------------------------------------\n\n\u003ca name=\"smax-linking\"\u003e\u003c/a\u003e\n## Linking your application against `smax-clib`\n\nProvided you have installed the shared (`libsmax.so`, `libredisx.so`, and `libxchange.so`) or static (`libsmax.a`, \n`libredisx.a`, and `libxchange.so`) libraries in a location that is in your `LD_LIBRARY_PATH` (e.g. in `/usr/lib` or \n`/usr/local/lib`) you can simply link your program using the  `-lsmax -lredisx -lxchange` flags. Your `Makefile` may \nlook like: \n\n```make\nmyprog: ...\n\t$(CC) -o $@ $^ $(LDFLAGS) -lsmax -lredisx -lxchange \n```\n\n(Or, you might simply add `-lsmax -lredisx -lxchange` to `LDFLAGS` and use a more standard recipe.) And, in if you \ninstalled the __smax-clib__, __RedisX__, and/or __xchange__ libraries elsewhere, you can simply add their location(s) \nto `LD_LIBRARY_PATH` prior to linking.\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"command-line-tools\"\u003e\u003c/a\u003e\n## Command-line tools\n\nThe __smax-clib__ library provides two basic command-line tools:\n\n - `smaxValue` -- for exploring the contents of SMA-X, including associated metadata, and\n - `smaxWrite` -- for putting data into SMA-X from the shell.\n \nThe tools can be compiled with `make tools`. Both tools can be run with the `--help` option for a simple help screen on\nusage. E.g.:\n\n```bash\n  $ smaxValue --help\n```\n\nThese command-line tools provide a simple means to interact with SMA-X from the shell or a scripting language, such\nas `bash`, or `perl` (also `python` though we recommend to use the native \n[Smithsonian/smax-python](https://github.com/Smithsonian/smax-python) library instead).\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"smax-configuration\"\u003e\u003c/a\u003e\n## Initial configuration\n\nBu default, the library assumes that the Redis server name used for SMA-X is either stored in the environment variable\n`SMAX_HOST` or is `smax` (e.g. you may assign `smax` to an IP address in `/etc/hosts`), and that the Redis is on \nthe default port 6379/tcp. However, you can configure to use a specific host and/or an alternative Redis port number \nalso, e.g.:\n\n```c\n  smaxSetServer(\"my-smax.example.com\", 7033);\n```\n\nAlso, while SMA-X will normally run on database index 0, you can also specify a different database number to use. E.g.:\n\n```c\n  smaxSetDB(3);\n```\n\n(Note, you can switch the database later also, but beware that if you have an active subscription client open, you cannot\nswitch that client until the subscriptions are terminated.)\n\nYou can also set up the authentication credentials for using the SMA-X database on the Redis server:\n\n```c\n  smaxSetUser(\"johndoe\");\n  smaxSetPassword(mySecretPassword);\n```\n\nBy default __smax_clib__ will connect both an interactive and a pipeline (high-throughput) Redis client. However, if you\nare planning to only use interactive mode (for setting an queries), you might not want to connect the pipeline client\nat all:\n\n```c\n  smaxSetPipelined(FALSE);\n```\n\nAnd finally, you can select the option to automatically try reconnect to the SMA-X server in case of lost connection or\nnetwork errors (and keep track of changes locally until then):\n\n```c\n  smaxSetResilient(TRUE);\n```\n\n### TLS configuration\n\nYou can also use SMA-X with a TLS encrypted connection. (We don't recommend using TLS with SMA-X in general though, \nsince it may adversely affect the performance / throughput of the database.) When enabled, Redis normally uses mutual \nTLS (mTLS), but it may be configured otherwise also. Depending on the server configuration, and the level of security \nrequired, you may configure some or all of the following options:\n\n```c\n  int status;\n\n  // Use TLS with the specified CA certificate file and path\n  status = smaxSetTLS(\"path/to/certificates\", \"ca.crt\");\n  if(status) {\n    // Oops, the CA certificate is not accessible...\n    ...\n  }\n  \n  // (optional) If servers requires mutual TLS, you will need to provide \n  // a certificate and private key also\n  status = smaxSetMutualTLS(\"path/to/redis.crt\", \"path/to/redis.key\");\n  if(status) {\n    // Oops, the certificate or key file is not accessible...\n    ...\n  }\n\n  // (optional) Skip verification of the certificate (insecure!)\n  smaxSetTLSVerify(FALSE);\n\n  // (optional) Set server name for SNI\n  smaxSetTLSServerName(\"my.smax-server.com\");\n\n  // (optional) Set ciphers to use (TLSv1.2 and earlier)\n  smaxSetTLSCiphers(\"HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4\");\n  \n  // (optional) Set cipher suites to use (TLSv1.3 and later)\n  smaxSetTLSCipherSuites(\"ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_256_GCM_SHA384\");\n  \n  // (optional) Set parameters for DH-based ciphers\n  status = smaxSetDHCypherParams(\"path/to/redis.dh\");\n  if(status) {\n    // Oops, the parameter file is not accessible...\n    ...\n  }\n```\n\n### Reconfiguration\n\nThe SMA-X configuration is activated at the time of connection (see below), after which it persists, through \nsuccessive connections also. That means, that once you have connected to the server, you cannot alter the \nconfiguration prior to another connection attempt, unless you call `smaxReset()` first while disconnected. \n`smaxReset()` will discard the currently configured Redis instance, so the next connection will create a new one, with \nthe current configuration.\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"smax-connecting\"\u003e\u003c/a\u003e\n## Connecting to / disconnecting from SMA-X \n\nOnce you have configured the connection parameters, you can connect to the configured (or default) server by:\n\n```c\n  int status = smaxConnect();\n  if(status \u003c 0) {\n     // Oops, we could not connect to the server\n     ...\n  }\n```\n\nAnd, when you are done, you should disconnect with:\n\n```c\n  smaxDisconnect();\n```\n\n\u003ca name=\"smax-connection-hooks\"\u003e\u003c/a\u003e\n### Connection / disconnection hooks\n\nThe user of the __smax-clib__ library might want to know when connections to the SMA-X server are established, or when \ndisconnections happen, and may want to perform some configuration or clean-up accordingly. For this reason, the \nlibrary provides support for connection hooks -- that is custom functions that are called in the even of connecting \nto or disconnecting from a Redis server.\n\nHere is an example of a connection hook, which simply prints a message about the connection to the console.\n\n```c\n  void my_connect_hook() {\n     printf(\"Connected to SMA-X server\\n\");\n  }\n```\n\nAnd, it can be activated prior to the `smaxConnect()` call.\n\n```c\n  smaxAddConnectHook(my_connect_hook);\n```\n\nThe same goes for disconnect hooks, using `smaxAddDisconnectHook()` instead.\n\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"sharing-and-pulling\"\u003e\u003c/a\u003e\n## Sharing and pulling data\n\n - [The basics](#smax-basics)\n - [Standard metadata](#metadata)\n - [Flexible types and sizes](#flexible-types-and-sizes)\n - [Scalar quantities](#smax-scalars)\n - [Arrays](#smax-arrays)\n - [Structures / substructures](#smax-structures)\n\n\u003ca name=\"smax-basics\"\u003e\u003c/a\u003e\n### The basics\n\nFor SMA-X we use the terms sharing and pulling, instead of the more generic get/set terminology. The intention is to\nmake programmers conscious of the fact that the transactions are not some local access to memory, but that they \ninvolve networking, and as such may be subject to unpredictable latencies, network outages, or other errors.\n\nAt the lowest level, the library provides two functions accordingly: `smaxShare()` and `smaxPull()`, either to send\nlocal data to store in SMA-X, or to retrieve data from SMA-X for local use, respectively. There are higher-level \nfunctions too, which build on these, providing a simpler API for specific data types, or extra features, such as \nlazy or pipelined pulls. These will be discussed in the sections below. But, before that, let's look into the basics\nof how data is handled between you machine local data that you use in your C/C++ application and its \nmachine-independent representation in SMA-X.\n\nHere is an example for a generic sharing of a `double[]` array from C/C++:\n\n```c\n  double data[8] = ...   // your local data\n\n  // Share (send) this data to SMA-X as \"system:subsystem:some_data\"\n  int status = smaxShare(\"system:subsystem\", \"some_data\", data, X_DOUBLE, 8);\n  if(status \u003c 0) {\n    // Ooops, that did not work...\n    ...\n  }\n  \n```\n\nPulling data back from SMA-X works similarly, e.g.:\n\n```c\n  double data[4][2] = ...   // buffer in which we will receive data\n  XMeta meta;               // (optional) metadata we can obtain \n  \n  // Retrieve 'data' from the SMA-X \"system:subsystem:some_data\", as 8 doubles\n  int status = smaxPull(\"system:subsystem\", \"some_data\", X_DOUBLE, 8, data, \u0026meta);\n  if(status \u003c 0) {\n    // Oops, something went wrong...\n    return;\n  }\n\n```\n\nThe metadata argument is optional, and can be `NULL` if not required.\n\n\n\u003ca name=\"metadata\"\u003e\u003c/a\u003e\n### Standard metadata\n\nFor every variable (structure or leaf node) in SMA-X there is also a set of essential metadata that is stored in the\nRedis database, which describe the data themselves, such as the native type (at the origin); the size as stored in \nRedis; the array dimension and shape; the host (and program) which provided the data; the time it was last updated;\nand a serial number.\n\nThe user of the library has the option to retrieve metadata together with the actual data, and thus gain access to\nthis information. The header file `smax.h` defines the `Xmeta` type as:\n\n\n```c\n  typedef struct XMeta {\n    int status;                       // Error code or X_SUCCESS.\n    XType storeType;                  // Type of variable as stored.\n    int storeDim;                     // Dimensionality of the data as stored.\n    int storeSizes[X_MAX_DIMS];       // Sizes along each dimension of the data as stored.\n    int storeBytes;                   // Total number of bytes stored.\n    char origin[SMAX_ORIGIN_LENGTH];  // Host name that last modified.\n    struct timespec timestamp;        // Timestamp of the last modification.\n    int serial;                       // Number of times the variable was updated.\n  } XMeta;\n```\n\n\u003ca name=\"flexible-types-and-sizes\"\u003e\u003c/a\u003e\n### Flexible types and sizes\n\nOne nifty feature of the library is that as a consumer you need not be too concerned about what type or size of data\nthe producer provides. The program that produces the data may sometimes change, for example from writing 32-bit \nfloating point types to a 64-bit floating point types. Also while it produced data for say 10 units before (as an \narray of 10), now it might report for just 9, or perhaps it now reports for 12. \n\nThe point is that if your consumer application was written to expect ten 32-bit floating floating point values, it can \nget that even if the producer changed the exact type or element count since you have written your client. The library \nwill simply apply the necessary type conversion automatically, and then truncate, or else pad (with zeroes), the data \nas necessary to get what you want.\n\nThe type conversion can be both widening or narrowing. Strings and numerical values can be converted to one another \nthrough the expected string representation of numbers and vice versa. Boolean `true` values are treated equivalently \nto a numerical value of 1 and all non-zero numerical values will convert to boolean `true`.\n\nAnd, if you are concerned about the actual type or size (or shape) of the data stored, you have the option to inspect\nthe metadata, and make decisions based on it. Otherwise, the library will just take care of giving you the available \ndata in the format you expect.\n\n\n\u003ca name=\"smax-scalars\"\u003e\u003c/a\u003e\n### Scalar quantities\n\nOften enough we deal with scalar quantities (not arrays), such as a single number, boolean value, or a\nstring (yes, we treat strings i.e., `char *`, as a scalar too!).\n\nHere are some examples of sharing scalars to SMA-X. Easy-peasy:\n\n```c\n  int status;\n\n  // Put a boolean value into SMA-X\n  status = smaxShareBoolean(\"system:subsystem\", \"is_online\", TRUE);\n\n  // Put an integer value into SMA-X\n  status = smaxShareInt(\"system:subsystem\", \"some_int_value\", 1012);\n  \n  // Put a floating-point value into SMA-X\n  status = smaxShareDouble(\"system:subsystem\", \"some_value\", -3.4032e-11);\n\n  // Put a string value into SMA-X\n  status = smaxShareString(\"system:subsystem\", \"name\", \"blah-blah\");\n```\n\nOr pulling them from SMA-X:\n\n```c\n  // Retrieve \"system:subsystem:is_online\" as a boolean, defaulting to FALSE\n  boolean isTrue = smaxPullBoolean(\"system:subsystem\", \"is_online\", FALSE);\n\n  // Retrieve \"system:subsystem:some_int_value\" as an int, with a default value of -1\n  int c = smaxPullInt(\"system:subsystem\", \"some_int_value\", -1);\n  \n  // Retrieve \"system:subsystem:some_value\" as a double (or else NAN if cannot).\n  double value = smaxPullDouble(\"system:subsystem\", \"some_value\");\n  if(!isnan(value)) {    // check for NAN if need be\n    ...\n  }\n\n  // Retrieve \"system:subsystem:name\" as a 0-terminated C string (or NULL if cannot).\n  char *str = smaxPullDouble(\"system:subsystem\", \"name\");\n  if(str != NULL) {      // check for NULL\n    ...\n  }\n \n  ...\n  \n  // Once the pulled string is no longer needed, destroy it.\n  free(str)\n\n```\n\n\u003ca name=\"smax-arrays\"\u003e\u003c/a\u003e\n### Arrays\n\nThe generic `smaxShare()` function readily handles 1D arrays, and the `smaxPull()` handles native (monolithic) arrays\nof all types (e.g. `double[][]`, or `boolean[][][]`). However, you may want to share multidimensional arrays, noting \ntheir specific shapes, or else pull array data for which you may not know in advance what size (or shape) of the \nstorage needed locally for the values stored in SMA-X for some variable. For these reasons, the library provides a set \nof functions to make the handling of arrays a simpler too.\n\nLet's begin with sharing multi-dimensional arrays. Instead of `smaxShare()`, you can use `smaxShareArray()` which \nallows you to define the multi-dimensional shape of the data beyond just the number of elements stored. E.g.:\n\n```c\n  float data[4][2] = ...    // Your local 2D data array\n  int shape[] = { 4, 2 };   // The array shape as stored in SMA-X\n  \n  int status = smaxShareArray(\"system:subsystem\", \"my_2d_array\", data, X_FLOAT, 2, shape);\n  if (status \u003c 0) {\n    // Oops, did not work...\n    ...\n  }\n```\n\nNote, that the dimensions of how the data is stored in SMA-X is determined solely by the '2' dimensions specified\nas the 4th argument and the corresponding 2 elements in the `shape` array. The `data` could have been any pointer\nto an array of floats containing at least the required number of element (8 in the example above).\n\nFor 1D arrays, you have some convenience methods for sharing specific types. These can be convenient because they \neliminate one potential source of bugs, where the type argument to `smaxShare()` does not match the pointer type of \nthe data. Using, say, `smaxShareFloats()` instead to share a 1D floating-point array instead of the generic \n`smaxShare()` will allow the compiler to check and warn you if the data array is not the `float *` type. E.g.:\n\n```c\n  float *data = ...  // pointer to a one or higher-dimensional C array of floats.\n\n  // Send N elements from data to SMA-X as \"system:subsystem:my_array\"\n  int status = smaxShareFloats(\"system:subsystem\", \"my_array\", data, N);\n  if (status \u003c 0) {\n    // Oops, did not work...\n    ...\n  }\n```\n\nSimilar functions are available for every built-in type (primitives, plus strings and booleans). For pulling arrays \nwithout knowing a-priori the element count or shape, there are also convenience functions, such as:\n\n```c\n  XMeta meta;    // (optional) we'll return the metadata in this\n  int status;\t // we'll return the status in this\n\n  // Get whatever data is in \"system:subsystem:my_array\" as doubles.\n  double *data = smaxPullDoubles(\"system:subsystem\", \"my_array\", \u0026meta, \u0026status);\n  if(status \u003c 0) {\n    // Oops, we got an error\n  } \n  if(data == NULL) {\n    // Oops the data is NULL\n  }\n  \n  ...\n  \n  // When done using the data we obtained, destroy it.\n  free(data);\n```\n\nAs illustrated, the above will return a dynamically allocated array with the required size to hold the data, and the\nsize and shape of the data is returned in the metadata that was also supplied with the call. After using the returned\ndata (and ensuring that it is not `NULL`), you should always call `free()` on it to avoid memory leaks in your\napplication.\n\n\u003ca name=\"smax-structures\"\u003e\u003c/a\u003e\n### Structures / substructures...\n\nYou can share entire data structures, represented by an appropriate `XStructure` type (see the __xchange__ library for \na description and usage):\n\n```c\n  XStructure s = ...   // The structured data you have prepared locally.\n  \n  int status = smaxShareStruct(\"system:subsystem\", \u0026s);\n  if(status \u003c 0) {\n    // Oops, something did not work\n    ...\n  }\n```\n\nOr, you can read a structure, including all embedded substructures in it, with `smaxPullStruct()`:\n\n\n```c\n  XMeta meta;\n  int nElements;\n  \n  XStructure *s = smaxPullStruct(\"system\", \"subsystem\", \u0026meta, \u0026n);\n  if(n \u003c 0) {\n    // Oops there was an error...\n    return;\n  }\n  \n  double value = smaxGetDoubleField(s, \"some_value\", 0.0);\n  \n  ...\n  \n  // Once the pulled structure is no longer needed, destroy it...\n  xDestroyStruct(s);\n```\n\nNote, that the structure returned by `smaxPullStruct()` is in the serialized format of SMA-X. That is, all leaf nodes\nare stored as strings, just as they appear in the Redis database. Hence, we used the `smaxGet...Field()` methods above \nto deserialize the leaf nodes as needed on demand. If you want to use the methods of __xchange__ to access the \nstructure, you will need to convert to binary format first, using `smax2xStruct(XStructure *)`.\n\nNote also, that pulling large structures can be an expensive operation on the Redis server, and may block the server \nfor longer than usual periods, causing latencies for other programs that use SMA-X. It's best to use this method for \nsmallish structures only (with, say, a hundred or so or fewer leaf nodes).\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"lazy-pulling\"\u003e\u003c/a\u003e\n## Lazy pulling (high-frequency queries)\n  \nWhat happens if you need the data frequently? Do you pound on the database at some high-frequency? No, you probably \nno not want to do that, especially if the data you need is not necessarily changing fast. There is no point on wasting\nnetwork bandwidth only to return the same values again and again. This is where 'lazy' pulling excels.\n\nFrom the caller's perspective lazy pulling works just like regular SMA-X pulls, e.g.:\n\n```c\n  int data[10][4][2];\n  int sizes[] = { 10, 4, 2 };\n  XMeta meta;\n\n  int status = smaxLazyPull(\"some_table\", \"some_data\", X_INT, 3, sizes, data, \u0026meta);\n```\n\nor\n\n```c\n  int status = smaxLazyPullDouble(\"some_table\", \"some_var\");\n```\n\nBut, under the hood, it does something different. The first time a new variable is lazy pulled it is fetched from the\nRedis database just like a regular pull. But, it also will cache the value, and watch for update notifications from\nthe SMA-X server. Thus, as long as no update notification is received, successive calls will simply return the locally\ncached value. This can save big on network usage, and also provides orders of magnitude faster access so long as the \nvariable remains unchanged.\n\nWhen the variable is updated in SMA-X, our client library will be notified, and one of two things can happen:\n\n 1. it invalidates the cache, so that the next lazy pull will again work just like a regular pull, fetching the \n    updated value from SMA-X on demand. And again the library will cache that value and watch for notifications for \n    the next update. Or,\n    \n 2. it will trigger a background process to update the cached value in the background with a pipelined \n    (high-throughput) pull. However, until the new value is actually fetched, it will return the previously cached\n    value promptly.\n    \nThe choice between the two is yours, and you can control which suits your need best. The default behavior for lazy pulls\nis (1), but you may call `smaxCache()` after the first pull of a variable, to indicate that you want to enable \nbackground cache updates (2) for it. The advantage of (1) is that it will never serve you outdated data even if there\nare significant network latencies -- but you may have to wait a little to fetch updates. On the other hand (2) will\nalways provide a recent value with effectively no latency, but this value may be outdated if there are delays on the\nnetwork updating the cache. The difference is typically at the micro-seconds level on a local LAN. However, (2) may \nbe preferable when you need to access SMA-X data from timing critical code blocks, where it is more important to ensure\nthat the value is returned quickly, rather than whether it is a millisecond too old or not.\n\nYou can also explicitly select the second behavior by using `smaxGetCached()` instead of `smaxLazyPull()`:\n\n```c\n  int status = smaxGetCached(\"some_table\", \"some_data\", X_INT, 3, sizes, data, \u0026meta);\n```\n\nIn either case, when you are done using lazy variables, you should let the library know that it no longer needs to watch\nupdates for these, by calling either `smaxLazyEnd()` on specific variables, or else `smaxLazyFlush()` to stop watching\nupdates for all lazy variables. (A successive lazy pull will automatically start watching for updates again, in case you\nwish to re-enable).\n\n```c\n  // Lazy pull a bunch of data (typically in a loop).\n  for(...) {\n    smaxLazyPull(\"some_table\", \"some_var\", ...);\n    smaxLaxyPull(...);\n    ...\n  }\n  \n  // Once we do not need \"some_table:some_var\" any more:\n  smaxLazyEnd(\"some_table\", \"some_var\");\n  \n  ...\n  \n  // And to stop lazy accessing all\n  smaxLazyFlush();\n```\n\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"pipelined-pulls\"\u003e\u003c/a\u003e\n## Pipelined pulling (high volume queries)\n\n - [Synchronization points and waiting](#lazy-synchronization)\n - [Callbacks](#lazy-callbacks)\n - [Finishing up](#lazy-finish)\n\nThe regular pulling of data from SMA-X requires a separate round-trip for each and every request. That is, successive \npulls are sent only after the responses from the prior pull has been received. A lot of the time is spent on waiting \nfor responses to come back. With round trip times in the 100 \u0026mu;s range, this means that this method of fetching data\nfrom SMA-X is suitable for obtaining at most a a few thousand values per second.\n\nHowever, sometimes you want to get access to a large number of values faster. This is what pipelined pulling is for.\nIn pipelined mode, a batch of pull requests are sent to the SMA-X Redis server in quick succession, without waiting\nfor responses. The values, when received are processed by a dedicated background thread. And, the user has an option\nof either waiting until all data is collected, or asking for as callback when the data is ready. \n\nAgain it works similarly to the basic pulling, except that you submit your pull request to a queue with \n`smaxQueue()`. For example:\n\n```c\n  double d;\t// A value we will fill\n  XMeta meta;   // (optional) metadata to fill (for the above value).\n\n  int status = smaxQueue(\"some_table\", \"some_var\", X_DOUBLE, 1, \u0026d, \u0026meta);\n```\n\nPipelined (batched) pulls have dramatic effects on performance. Rather than being limited by round-trip times, you will\nbe limited by the performance of the Redis server itself (or the network bandwidth on some older infrastructure). As \nsuch, instead of thousand of queries per second, you can pull 2-3 orders of magnitude more in a given time, with \nhundreds of thousands of pull per second this way.\n\n\u003ca name=\"lazy-synchronization\"\u003e\u003c/a\u003e\n### Synchronization points and waiting\n\nAfter you have submitted a batch of pull request to the queue, you can create a synchronization point as:\n\n```c\n  XSyncPoint *syncPoint = smaxCreateSyncPoint();\n```\n\nA synchronization point is a marker in the queue that we can wait on. After the synchronization point is created, you\ncan submit more pull request to the same queue (e.g. for another processing block), or do some other things for a bit\n(since it will take at least some microseconds before the data is ready). Then, when ready you can wait on the \nspecific synchronization point to ensure that data submitted prior to its creation is delivered from SMA-X:\n\n```c\n  // Wait for data submitted prior to syncPoint to be ready, or time out after 1000 ms.\n  int status = smaxSync(syncPoint, 1000);\n  \n  // Destroy the synchronization point if we no longer need it.\n  xDestroySyncPoint(syncPoint);\n  \n  // Check return status...\n  if(status == X_TIMEOUT) {\n    // We timed out\n    ...\n  }\n  else if(status \u003c 0) {\n    // Some other error\n    ...\n  }\n```\n\n\u003ca name=\"lazy-callbacks\"\u003e\u003c/a\u003e\n### Callbacks\n\nThe alternative to synchronization points and waiting, is to provide a callback function, which will process your data\nas soon as it is available, e.g.:\n\n```c\n  void my_pull_processor(void *arg) {\n     // Say, we expect a string tag passed along to identify what we need to process...\n     char *tag = (char *) arg;\n     \n     // Do what we need to do...\n     ...\n  }\n```\n\nThen submit this callback routine to the queue after the set of variables it requires with:\n\n```c\n  // We'll call my_pull_processor, with the argument \"some_tag\", when prior data has arrived.\n  smaxQueueCallback(my_pull_processor, \"some_tag\");\n```\n\n\u003ca name=\"lazy-finish\"\u003e\u003c/a\u003e\n### Finishing up\n\nIf you might still have some pending pipelined pulls that have not received responses yet, you may want to wait until\nall previously submitted requests have been collected. You can do that with:\n\n```c\n  // Wait for up to 3000 ms for all pipelined pulls to collect responses from SMA-X.\n  int status = smaxWaitQueueComplete(3000);\n  \n  // Check return status...\n  if(status == X_TIMEOUT) {\n    // We timed out\n    ...\n  }\n  else if(status \u003c 0) {\n    // Some other error\n    ...\n  }\n```\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"update-handling\"\u003e\u003c/a\u003e\n## Custom update handling\n\n - [Monitoring updates](#monitoring-updates)\n - [Waiting for updates](#waiting-for-updates)\n - [Update callbacks](#update-callbacks)\n\n\n\u003ca name=\"monitoring-updates\"\u003e\u003c/a\u003e\n### Monitoring updates\n\nThe LUA scripts that define SMA-X interface on the Redis server send out PUB/SUB notifications for every variable on \ntheir own dedicate PUB/SUB channel whenever the variable is updated. By default, lazy access methods subscribe to \nthese messages and use them to determine when to invalidate the cache and fetch new values from the database again. \nHowever, you may subscribe and use these messages outside of the lazy update routines also. The only thing you need to \npay attention to is not to unsubscribe from update notifications for those variables that have multiple active monitors\n(including lazy updates).\n\nOne common use case is that you want to execute some code in your application when some value changes in SMA-X. You\ncan do that easily, and have two choices on how you want to trigger the code execution: (1) you can block execution of\nyour current thread until an update notification is received for one of the variables (or patterns) of interest to \nwhich you have subscribed, or (2) you can let __smax_clib__ call a designated function of your application when such \nan update notification is captured. We'll cover these two cases separately below.\n\nHowever, in either case you will have to subscribe to the variable(s) or pattern(s) of interest with `smaxSubscribe()`\nbefore updates can be processed, e.g.\n\n```c\n  int status = smaxSubscribe(\"some_table\", \"watch_point_variable\");\n  if (status \u003c 0) {\n    // Ooops something did not go to plan\n    ...\n  }\n```\n\nand/or pattern(s):\n\n```c\n  int status = smaxSubscribe(\"*\", \"b[a-c]?\");\n  if (status \u003c 0) {\n    // Ooops something did not go to plan\n    ...\n  }\n```\n\nYou can subscribe to any number of variables or patterns in this way. __smax_clib__ will receive and process \nnotifications for all of them. (So beware of creating unnecessary network traffic.)\n\n\u003ca name=\"waiting-for-updates\"\u003e\u003c/a\u003e\n### Waiting for updates\n\nThe first option for executing code conditional on some variable update is to block execution in the current thread\nand wait until the variable(s) of interest change(s) (or until some timeout limit is reached). There is a group of\nfunctions `smaxWaitOn...()` that do exactly that, provided you have already subscribed to receiving updates for\nthe desired variables / patterns. For example:\n\n```c\n  // Wait for foo:bar to update, or wait at most 500 ms\n  int status = smaxWaitOnSubscribedVar(\"foo\", \"bar\", 500, NULL);\n  if (status == X_TIMEDOUT) {\n    // Wait timed out, maybe we want to try again, or do something else...\n    ...\n  }\n  else if (status \u003c 0) {\n    // Oh no, some other error happened.\n    ...\n  }\n  ...\n\n```\n\nSimilar methods allow you to wait for updates on any subscribed variable in selected tables, or the update of select \nvariables in all subscribed tables, or any of the subscribed variables to change for the wait to end normally (with \nreturn value 0). \n\nThe last parameter (`NULL` in the above example) allows to pass a pointer to a POSIX sempahore, which can be used for \ngating another thread, which might also want exclusive access to the SMA-X notifications, but we do not want it to\naccidentally block entering the wait in a timely manner.\n\n\n\u003ca name=\"update-callbacks\"\u003e\u003c/a\u003e\n### Update callbacks\n\nSometimes, you don't want to block execution, but you want to make sure some code executes when the a variable\nor variables of interest get updated. For such cases you can designate your own `RedisSubscriberCall` callback \nfunction, e.g.:\n\n```c\n  void my_update_processor(const char *pattern, const char *channel, const char *msg, long len) {\n     const char *varID;  // The variable aggregate ID. \n  \n     // check that it's a SMA-X update -- channel should begin with SMAX_UPDATE\n     if (strncmp(channel, SMAX_UPDATE, SMAX_UPDATE_LENGTH) != 0) {\n       // Not an SMA-X variable update. It's not what we expected.\n       return;\n     }\n     \n     id = \u0026channel[SMAX_UPDATE_LENGTH];\n     // Now we can check the ID to see which of the variables updated, and act accordingly.\n     ...\n  }\n```\n\nOnce you have defined your callback function, you can activate it with `smaxAddSubscriber()`, e.g.:\n\n```c\n  // Call my_update_processor on updates for any subscribed variable whose table name starts with \"watch_table_\"\n  int status = smaxAddSubscriber(\"watch_table_\", my_update_processor);\n  if (status \u003c 0) {\n    // Did not go to plan.\n    ...\n  }\n```\n  \nWhen you no longer need to process such updates, you can simply remove the function from being called via \n`smaxRemoveSubscriber()`, and if you are absolutely sure that no other part of your code needs the subscription(s)\nthat could trigger it, you can also unsubscribe from the trigger variables/pattern to eliminate unnecessary network \ntraffic.\n\nOne word of caution on callbacks is that they are expected to:\n\n - execute quickly\n - never block for prolonged periods. (It may be OK to wait briefly on a mutex, provided nothing else can hog that \n   mutex for prolonged periods).\n  \nIf the above two conditions cannot be guaranteed, it's best practice for your callback to place a copy of the \ncallback information on a queue, and then spawn or notify a separate thread to process the information in the \nbackground, including discarding the copied data if it's no longer needed. Alternatively, you can launch a dedicated\nprocessor thread early on, and inside it wait for the updates before executing some complex action. The choice is\nyours.\n\n\n------------------------------------------------------------------------------  \n\n\u003ca name=\"remote-control\"\u003e\u003c/a\u003e  \n## Remote program control via SMA-X\n\n - [Server side](#server-side)\n - [Client side](#client-side)\n - [Complex remote control calls and return values](#complex-control)\n\nIt is possible to use SMA-X for remote control of programs on distributed systems. In effect, any client can set \ndesignated control variables / values. These variables are monitored by an appropriate server program, which acts to \nchanges to the 'commanded' values accordingly, and report the result back in a related other SMA-X variable. The \nclient thus can obtain confirmation from the response variable after it submits it requested 'command' variable.\n\n\u003ca name=\"server-side\"\u003e\u003c/a\u003e\n### Server side\n\nOn the server side, you will need a function (of `SMAXControlFunction` type), which acts when some control variable \nchanges. E.g.:\n\n```c\n  // the function will be called with the SMA-X hash table and control variable names \n  // which triggered the call, as well as an optional user-supplied pointer argument.\n  int my_control_function(const char *table, const char *key, void *parg) {\n    // Let's assume the pointer argument defines what variable we use for the response...\n    const char *replyKey = (const char *) parg;\n    \n    // Let's say we expect an integer control value...\n    // We'll pull it from SMA-X as such, or default to -1 if the value is not an integer.\n    int value = smaxPullInt(table, key, -1);\n  \n    // We could do something with the requested value...\n    ...\n  \n    // Finally we'll write the requested (or actual) value to the reply keyword to \n    // indicate completion.\n    return smaxShareInt(table, replyKey, value);\n  }\n```\n\nIt is important to remember that each action taken on a control variable should the set the designated response\nvariable exactly once, and only when the action is completed, so that the calling client can use it to confirm \nthe completed action. The same action action is free to set any number of other values in SMA-X before signaling \ncompletion, thus 'returning' further data to the caller, if needed. \n\nNext, all you have to do is specify what control variable to use this function with and what optional pointer\nargument to pass on to it:\n\n```c\n  // We'll trigger the function whenever 'system:subsystem:control_value' changes.\n  // And we'll send a response to 'actual_value' in the same hash table.\n  int status = smaxSetControlFunction(\"system:subsystem\", \"control_value\", my_control_function, \"actual_value\");\n  if(status \u003c 0) {\n    // Oops, something went wrong...\n    return -1;\n  }\n```\n\nThe above call will subscribe for updates to `system:subsystem:control_value` and will call `my_control_function` with\n`actual_value` as the optional argument. You may change the function called later, or undefine it by calling \n`smaxSetControlFunction()` with `NULL` as the function pointer.\n\nClearly, the same processing function can be used with multiple control values, if convenient, or you may specify \ndifferent control functions to every control value if it makes more sense for the implementation.\n\nOne thing to watch out for on the server-side implementation is that control functions are called asynchronously and \nimmediately, each time the control variable updates. As such, a control function may be called while another, or\neven the same one, is in the middle of performing its task for a prior 'command'. You should therefore use mutexes as \nnecessary to prevent the concurrent execution of program controls as appropriate. E.g.\n\n```c\n  // A mutex to prevent concurrent execution of control calls...\n  static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;\n \n  int my_control_function(const char *table, const char *key, void *parg) {\n    // Ensure we are executing the control code exclusively, with regard to other calls\n    // to the same function, and any other control function that locks the same mutex...    \n    pthread_mutex_lock(\u0026mutex);\n    \n    // Perform the program control\n    ...\n    \n    // We are done. Let other control calls execute now...\n    phtread_mutex_unlock(\u0026mutex);\n  \n    return status;\n  }\n```\n\n\u003ca name=\"client-side\"\u003e\u003c/a\u003e\n### Client side\n\nFrom the client side, you control the above server by setting `system:subsystem:control_value` to an appropriate new \nvalue, and then wait for the response in `system:subsystem:actual_value`. You can do that via `smaxControl()` or one \nof its type-specific variants. Since in the above server example, we use integer control value and reply, we'll use\n`smaxControlInt()`:\n\n```c\n  // We'll set the control value to 42, and wait for a response for up to 5 seconds, \n  // or else return -1.\n  int reply = smaxControlInt(\"system:subsystem\", \"control_value\", 42, NULL, \"actual_value\", -1, 5);\n  if(reply != 42) {\n    // Oops, no luck\n    ...\n  }\n```\n\nThe `NULL` as the 3rd argument is a shorthand to indicate that we expect the reply in the same hash table in which we\nset `control_value` (that is in `system:subsystem`). If we expect the response in some other location, we can specify\nthe appropriate table name instead of the `NULL` pointer in the example above.\n\n\n\u003ca name=\"complex-control\"\u003e\u003c/a\u003e\n### Complex remote control calls and return values\n\nAs hinted earlier, the remote control via SMA-X relies on a single control variable, and a single response variable\nprovides confirmation of completion back to the client. This scheme does not preclude passing multiple values to the\nserver, or receiving multiple values as a response. A client may set a number of call parameters in SMA-X before \ntriggering the action on them by the designated control variable. The server will not act on the parameters alone. \nInstead, only when the designated control (trigger) variable is set it proceeds to read all associated parameter \nvalues from SMA-X.\n\nSimilarly, the server may set a number of return parameters during its action, before finally setting the designated\nresponse variable to indicate completion. For example, consider locking a local oscillator to a designated frequency\nand sideband (two parameters). The client might do that by:\n\n```c\n  // Set the frequency and sideband parameters first...\n  smaxShareDouble(\"system:lo\", \"lock_frequency\", 230.5e9);\n  smaxShareString(\"system:lo\", \"lock_sideband\", \"usb\");\n  \n  // Then trigger the action on the remote server by setting \"lock\" to 1, waiting \n  // for confirmation in \"is_locked\", defaulting to -1 in case of failure.\n  int reply = smaxControlInt(\"system:lo\", \"lock\", 1, NULL, \"is_locked\", -1, timeout);\n  \n  if(reply == -1) {\n    // Oops, something went wrong\n    ...\n  }\n  else if(reply == 0) {\n    fprintf(stderr, \"WARNING! LO failed to lock\\n\");\n  }\n  else {\n    // Read the actual values sent in response in \"current_frequency\" and \"current_sideband\"\n    double freq = smaxPullDouble(\"system:lo\", \"current_frequency\");\n    char *sideband = smaxPullString(\"system:lo\", \"current_sideband\");\n \n    // Do whatever with the returned current values... \n    ...\n    \n    // Clean up\n    if(sideband) free(sideband);\n  }\n```\n\nAnd the server might process the request as:\n\n```c\n  int lock_function(const char *table, const char *key, void *parg) {\n    double freq = smaxPullDouble(\"system:lo\", \"lock_frequency\");\n    char *sideband = smaxPullString(\"system:lo\", \"lock_sideband\");\n    \n    // Perform the locking with the above parameters...\n    ...\n    \n    // Write back the actual frequency and sideband\n    smaxShareDouble(\"system:lo\", \"current_frequency\", current_freq);\n    smaxShareString(\"system:lo\", \"current_sideband\", current_sb);\n    \n    // Clean up...\n    if(sideband) free(sideband);\n    \n    // Finally indicate lock status and completion\n    return smaxShareInt(\"system:lo\", \"is_locked\", is_locked);\n  }\n```\n\n\n------------------------------------------------------------------------------  \n\n\u003ca name=\"status-messages\"\u003e\u003c/a\u003e  \n## Program status / error messages via SMA-X\n\n - [Broadcasting status messages from an application](#smax-broadcasting)\n - [Processing program messages](#smax-processing-messages)\n\nSMA-X also provides a standard for reporting program status, warning, and error messages via the Redis PUB/SUB \ninfrastructure. \n \n\u003ca name=\"smax-broadcasting\"\u003e\u003c/a\u003e\n### Broadcasting status messages from an application\n\nBroadcasting program messages to SMA-X is very simple using a set of dedicated messaging functions by message\ntype. These are:\n\n | __smax_clib__ function                                     | Description                                |\n |------------------------------------------------------------|--------------------------------------------|\n | `smaxSendStatus(const char *msg, ...)`                     | sends a status message                     |\n | `smaxSendInfo(const char *msg, ...)`                       | sends an informational message             |\n | `smaxSendDetail(const char *msg, ...)`                     | sends optional status/information detail   |\n | `smaxSendDebug(const char *msg, ...)`                      | sends a debugging messages                 |\n | `smaxSendWarning(const char *msg, ...)`                    | sends a warning message                    |\n | `smaxSendError(const char *msg, ...)`                      | sends an error message                     |\n | `smaxSendProgress(double fraction, const char *msg, ...)`  | sends a progress update and message        |\n\nAll the above methods work like `printf()`, and can take additional parameters corresponding to the format specifiers\ncontained in the `msg` argument.\n\nBy default, the messages are sent under the canonical program name (i.e. set by `_progname` on GNU/Linux systems) \nthat produced the message. You can override that, and define a custom sender ID for your status messages, by calling\n`smaxSetMessageSenderID()` prior to broadcasting, e.g.:\n\n```c\n  // Set out sender ID to \"my_program_id\"\n  smaxSetMessageSenderID(\"my_program_id\");\n  \n  ...\n  \n  // Broadcast a warning message for \"my_program_id\"\n  int status = smaxSendWarning(\"Something did not work\" %s\", explanation);\n```\n\n\u003ca name=\"smax-processing-messages\"\u003e\u003c/a\u003e\n### Processing program messages\n\nOn the receiving end, other applications can process such program messages, for a selection of hosts, programs, and \nmessage types. You need to prepare you message processor function(s) first, e.g.:\n\n```c\n  void my_message_processor(XMessage *m) {\n    printf(\"Received %s message from %s: %s\\n\", m-\u003etype, m-\u003eprog, m-\u003etext);\n  }\n  \n```\n\nThe processor function does not return any value, since it is called by a background thread, which does not check for\nreturn status. The `XMessage` type, a pointer to which is the sole argument of the processor, is defined in `smax.h` \nas:\n\n\n```c\n  typedef struct {\n    char *host;                   // Host where message originated from\n    char *prog;                   // Originator program name\n    char *type;                   // Message type, e.g. \"info\", \"detail\", \"warning\", \"error\"\n    char *text;                   // Message body (with timestamp stripped).\n    double timestamp;             // Message timestamp, if available (otherwise 0.0)\n  } XMessage;\n```\n\nOnce you have your message consumer function, you can set it to be called for messages from select hosts, programs, and/or\nselect message types, using `smaxAddMessageProcessor()`, e.g.:\n\n```c\n  // Will call my_message_procesor for all messages coming from \"my_program_id\" from all hosts.\n  // The return ID number (if \u003e 0) can be used later to uniquely identify the processor with the set of selection \n  // parameters it is used with. So make sure to keep it handy for later.\n  int id = smaxAddMessageProcessor(\"*\", \"my_program_id\", \"*\", my_message_processor);\n  if (id \u003c 0) {\n    // Oops that did not work as planned.\n    ...\n  }\n```\n\nEach string argument (`host`, `prog`, and `type`) may take an asterisk (`\"*\"`) or `NULL` as the argument to indicate that\nthe processor function should be called for incoming messages for all values for the given parameter.\n\nThe processor function can also inspect what type of message it received by comparing the `XMessage` `type` value against\none of the predefined constant expressions in `smax.h`:\n\n | `XMessage` `type`          | Description                                     |\n |----------------------------|-------------------------------------------------|\n | `SMAX_MSG_STATUS`          | status update                                   |\n | `SMAX_MSG_INFO`            | informational program message                   |\n | `SMAX_MSG_DETAIL`          | additional detail (e.g. for verbose messages).  |\n | `SMAX_MSG_PROGRESS`        | progress update.                                |\n | `SMAX_MSG_DEBUG`           | debug messages (also e.g. traces)               |\n | `SMAX_MSG_WARNING`         | warning message                                 |\n | `SMAX_MSG_ERROR`           | error message                                   |\n\nOnce you no longer need to process messages by the given processor function, you can remove it from the call list by\npassing its ID number (\u0026lt;0) to `smaxRemoveMessageProcessor()`.\n\n------------------------------------------------------------------------------\n\n\u003ca name=\"optional-metadata\"\u003e\u003c/a\u003e\n## Optional metadata\n\n\n### Descriptions\n\n### Coordinate Systems\n\n### Physical units\n\n\n-----------------------------------------------------------------------------\n\n\u003ca name=\"smax-error-handling\"\u003e\u003c/a\u003e\n## Error handling\n\nThe principal error handling of the library is an extension of that of __xchange__, with further error codes defined \nin `smax.h` and `redisx.h`. The functions that return an error status (either directly, or into the integer designated \nby a pointer argument), can be inspected by `smaxErrorDescription()`, e.g.:\n\n```c\n  int status = smaxShare(...);\n  if (status != X_SUCCESS) {\n    // Ooops, something went wrong...\n    fprintf(stderr, \"WARNING! set value: %s\", smaxErrorDescription(status));\n    ...\n  }\n```\n\n-----------------------------------------------------------------------------\n\n\u003ca name=\"smax-debug-support\"\u003e\u003c/a\u003e\n## Debug support\n\nYou can enable verbose output of the library with `smaxSetVerbose(boolean)`. When enabled, it will produce status \nmessages to `stderr`so you can follow what's going on. In addition (or alternatively), you can enable debug messages \nwith `xSetDebug(boolean)`. When enabled, all errors encountered by the library (such as invalid arguments passed) will \nbe printed to `stderr`, including call traces, so you can walk back to see where the error may have originated from. \n(You can also enable debug messages by default by defining the `DEBUG` constant for the compiler, e.g. by adding \n`-DDEBUG` to `CFLAGS` prior to calling `make`). \n\nFor helping to debug your application, the __xchange__ library provides two macros: `xvprintf()` and `xdprintf()`, \nfor printing verbose and debug messages to `stderr`. Both work just like `printf()`, but they are conditional on \nverbosity being enabled via `xSetVerbose(boolean)` and `xSetDebug(boolean)`, respectively. Applications using this \nlibrary may use these macros to produce their own verbose and/or debugging outputs conditional on the same global \nsettings. \n\n\n\n-----------------------------------------------------------------------------\n\n\u003ca name=\"smax-future-plans\"\u003e\u003c/a\u003e\n## Future plans\n\nSome obvious ways the library could evolve and grow in the not too distant future:\n\n - Automated regression testing and coverage tracking.\n\nIf you have an idea for a must have feature, please let me (Attila) know. Pull requests, for new features or fixes to\nexisting ones, are especially welcome! \n\n-----------------------------------------------------------------------------\n\n\u003ca name=\"smaxlib-release-schedule\"\u003e\u003c/a\u003e\n## Release schedule\n\nA predictable release schedule and process can help manage expectations and reduce stress on adopters and developers \nalike.\n\n__smax-clib__ will try to follow a quarterly release schedule. You may expect upcoming releases to be published around \n__March 1__, __June 1__, __September 1__, and/or __December 1__ each year, on an as-needed basis. That means that if \nthere are outstanding bugs, or new pull requests (PRs), you may expect a release that addresses these in the upcoming \nquarter. The dates are placeholders only, with no guarantee that a new release will actually be available every \nquarter. If nothing of note comes up, a potential release date may pass without a release being published.\n\nNew features are generally reserved for the feature releases (e.g. __1.x.0__ version bumps), although they may also be \nrolled out in bug-fix releases as long as they do not affect the existing API -- in line with the desire to keep \nbug-fix releases fully backwards compatible with their parent versions.\n\nIn the weeks and month(s) preceding releases one or more _release candidates_ (e.g. `1.0.1-rc3`) will be published \ntemporarily on GitHub, under [Releases](https://github.com/Smithsonian/smax-clib/releases), so that changes can be \ntested by adopters before the releases are finalized. Please use due diligence to test such release candidates with \nyour code when they become available to avoid unexpected surprises when the finalized release is published. Release \ncandidates are typically available for one week only before they are superseded either by another, or by the finalized \nrelease.\n\n \n-----------------------------------------------------------------------------\nCopyright (C) 2025 Attila Kovács\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmithsonian%2Fsmax-clib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmithsonian%2Fsmax-clib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmithsonian%2Fsmax-clib/lists"}