---
title: "C/C++ API"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{C/C++ API}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
`unigd` exposes a C API that allows other R packages to interact with the
graphics device from compiled code. This enables building applications like plot
viewers, web servers, or IDE integrations on top of `unigd`.
The API has `extern "C"` linkage, so it can be used from both C and C++ client
code. It is a versioned vtable (struct of function pointers) obtained at runtime
through R's `R_GetCCallable` mechanism. All functions are thread-safe, so
clients can call into `unigd` from background threads.
The examples in this document use C++ for brevity, but the API itself is plain C.
## Architecture overview
```
R session
|
| ugd() opens a unigd graphics device
v
unigd device stores plot history, manages renderers
|
| C API function pointer table (unigd_api_v1)
v
client package attaches via device_attach(), receives callbacks,
renders plots on demand
```
The API follows a **client model**: a client package registers itself with an
open `unigd` device and receives lifecycle callbacks. It can then query device
state, browse plot history, and render plots to any supported format.
## Package setup
### DESCRIPTION
Your package needs to link against `unigd`:
```
Imports: unigd
LinkingTo: unigd
```
`Imports` ensures `unigd` is loaded (and its shared library available) before
your package. `LinkingTo` gives your compiler access to the headers in
`unigd/inst/include/`.
### Include the API header
In your C/C++ source files:
```c
#include
```
This single header provides `unigd_api_v1_create()` and
`unigd_api_v1_destroy()` (factory functions that obtain the API vtable via
`R_GetCCallable`), as well as all type definitions from ``
(structs, handle types, and typedefs).
## Initialization
Obtain the API vtable once when your package is loaded. The recommended pattern
uses a RAII guard and the `[[cpp11::init]]` attribute (which runs at
`R_init_` time):
```cpp
#include
#include
namespace mypackage {
// Global API pointer and client ID
unigd_api_v1 *api = nullptr;
UNIGD_CLIENT_ID client_id = 0;
namespace {
class api_guard {
public:
~api_guard() { destroy(); }
void create() {
if (api == nullptr) unigd_api_v1_create(&api);
}
void destroy() {
if (api != nullptr) {
unigd_api_v1_destroy(api);
api = nullptr;
}
}
};
api_guard guard;
} // namespace
} // namespace mypackage
[[cpp11::init]] void import_unigd_api(DllInfo *dll) {
mypackage::guard.create();
mypackage::client_id = mypackage::api->register_client_id();
}
```
After this runs, `mypackage::api` points to the vtable and
`mypackage::client_id` holds a unique identifier for your package. The guard
ensures `unigd_api_v1_destroy` is called when the shared library unloads.
## Types
### Handle types
The API uses opaque `void *` handles for resource management. Each
`*_create` / `*_find` function returns a handle that **must** be freed with the
corresponding `*_destroy` function.
| Type | Returned by | Freed by |
|-------------------------------|--------------------------|-----------------------------|
| `UNIGD_HANDLE` | `device_attach` | `device_destroy` |
| `UNIGD_RENDER_HANDLE` | `device_render_create` | `device_render_destroy` |
| `UNIGD_FIND_HANDLE` | `device_plots_find` | `device_plots_find_destroy` |
| `UNIGD_RENDERERS_HANDLE` | `renderers` | `renderers_destroy` |
| `UNIGD_RENDERERS_ENTRY_HANDLE`| `renderers_find` | `renderers_find_destroy` |
### ID types
| Type | Underlying | Description |
|-----------------------|---------------|------------------------------------------|
| `UNIGD_PLOT_ID` | `uint32_t` | Stable identifier for a plot |
| `UNIGD_PLOT_INDEX` | `uint32_t` | Positional index (oldest = 1) |
| `UNIGD_PLOT_RELATIVE` | `int32_t` | Signed offset (0 = latest, -1 = previous)|
| `UNIGD_CLIENT_ID` | `uint32_t` | Unique client identifier |
| `UNIGD_RENDERER_ID` | `const char *`| Renderer string ID (e.g. `"svg"`, `"png"`)|
### Structs
#### `unigd_graphics_client`
Callback table provided by the client to `device_attach`. All callbacks receive
the `client_data` pointer that was passed to `device_attach`.
```c
struct unigd_graphics_client {
void (*start)(void *client_data); // Device activated
void (*close)(void *client_data); // Device closing
void (*state_change)(void *client_data); // Plot state changed
const char *(*info)(void *client_data); // Return client info string
};
```
- **`start`**: Called once after `device_attach` succeeds. Use this to
launch background threads or initialize resources.
- **`close`**: Called when the device is closed (e.g. `dev.off()` in R).
Clean up resources and call `device_destroy` on the handle here. After this
returns, the device handle is no longer valid.
- **`state_change`**: Called whenever a plot is created, modified, or removed.
Typically used to notify connected clients (e.g. push a WebSocket message).
- **`info`**: Should return a static string identifying the client
(e.g. `"mypkg 1.0.0"`).
#### `unigd_device_state`
```c
struct unigd_device_state {
int upid; // Update ID, increments on every state change
UNIGD_PLOT_INDEX hsize; // Number of plots in history
bool active; // Whether the device is active
};
```
The `upid` field is useful for change detection: if the `upid` hasn't changed
since the last check, no plots have been added, removed, or redrawn.
#### `unigd_render_args`
```c
struct unigd_render_args {
double width; // Plot width in inches (or pixels for raster)
double height; // Plot height in inches (or pixels for raster)
double scale; // Zoom/scale factor (1.0 = 100%)
};
```
Pass `width = -1` and `height = -1` to use the device's current dimensions.
#### `unigd_render_access`
```c
struct unigd_render_access {
const uint8_t *buffer; // Pointer to rendered data
uint64_t size; // Size in bytes
};
```
The buffer is valid until the corresponding `UNIGD_RENDER_HANDLE` is destroyed.
#### `unigd_find_results`
```c
struct unigd_find_results {
unigd_device_state state; // Device state at query time
UNIGD_PLOT_INDEX size; // Number of results
UNIGD_PLOT_ID *ids; // Array of plot IDs
};
```
#### `unigd_renderer_info`
```c
struct unigd_renderer_info {
UNIGD_RENDERER_ID id; // e.g. "svg", "png"
const char *mime; // e.g. "image/svg+xml"
const char *fileext; // e.g. "svg"
const char *name; // Human-readable name
const char *type; // Renderer category
const char *description; // Short description
bool text; // true if output is text, false if binary
};
```
## Attaching to a device
A client attaches to an open `unigd` device by providing a callback struct, its
client ID, and an arbitrary data pointer:
```cpp
class MyClient {
unigd_api_v1 *m_api = nullptr;
UNIGD_HANDLE m_handle = nullptr;
unigd_graphics_client m_client;
public:
MyClient() {
m_client.start = [](void *d) {
static_cast(d)->on_start();
};
m_client.close = [](void *d) {
static_cast(d)->on_close();
};
m_client.state_change = [](void *d) {
static_cast(d)->on_state_change();
};
m_client.info = [](void *) { return "mypkg 1.0.0"; };
}
bool attach(int devnum) {
m_api = mypackage::api;
m_handle = m_api->device_attach(
devnum, // R device number
&m_client, // callback table
mypackage::client_id, // from register_client_id()
this // client_data passed to callbacks
);
return m_handle != nullptr;
}
void on_start() {
// Called after attach succeeds.
// Launch background work here.
}
void on_state_change() {
auto state = m_api->device_state(m_handle);
// React to state.upid, state.hsize, etc.
}
void on_close() {
// Device is closing. Clean up.
if (m_api && m_handle) {
m_api->device_destroy(m_handle);
}
delete this;
}
};
```
The `devnum` parameter is the R graphics device number, typically obtained from
`dev.cur()` after opening a `ugd()` device.
## Querying device state
```cpp
unigd_device_state state = api->device_state(handle);
// state.upid incremented on every change
// state.hsize total number of plots in history
// state.active false after dev.off()
```
## Browsing plot history
Use `device_plots_find` to query plot IDs with pagination:
```cpp
unigd_find_results results;
UNIGD_FIND_HANDLE fh = api->device_plots_find(
handle,
0, // offset: 0 = start from latest, negative = relative
10, // limit: max number of results (0 = all)
&results
);
for (UNIGD_PLOT_INDEX i = 0; i < results.size; ++i) {
UNIGD_PLOT_ID id = results.ids[i];
// ... use the plot ID ...
}
// Always free the results
api->device_plots_find_destroy(fh);
```
To remove a single plot or clear all history:
```cpp
api->device_plots_remove(handle, plot_id); // returns true on success
api->device_plots_clear(handle); // returns true on success
```
## Rendering plots
Rendering is a two-step process: create the render (which may trigger a redraw),
then access the buffer.
```cpp
// 1. Look up the renderer (optional, useful to get MIME type)
unigd_renderer_info rinfo;
auto rinfo_h = api->renderers_find("svg", &rinfo);
// rinfo.mime == "image/svg+xml"
// rinfo.text == true
// 2. Render a specific plot
unigd_render_access render;
auto render_h = api->device_render_create(
handle,
"svg", // renderer ID
plot_id, // from device_plots_find
{720.0, 576.0, 1.0}, // {width, height, scale}
&render
);
if (render_h) {
// render.buffer contains the SVG data
// render.size is the byte length
std::string svg(render.buffer, render.buffer + render.size);
}
// 3. Free resources
api->device_render_destroy(render_h);
api->renderers_find_destroy(rinfo_h);
```
To enumerate all available renderers:
```cpp
unigd_renderers_list list;
auto rh = api->renderers(&list);
for (uint64_t i = 0; i < list.size; ++i) {
const auto &r = list.entries[i];
// r.id, r.mime, r.fileext, r.name, r.text, ...
}
api->renderers_destroy(rh);
```
## Retrieving a client from a device
If your R code needs to look up a previously attached client (e.g. to expose
its state to R), use `device_get`:
```cpp
void *data = api->device_get(devnum, mypackage::client_id);
if (data) {
auto *client = static_cast(data);
// ... use client ...
}
```
This returns the `client_data` pointer that was passed to `device_attach`.
## Logging
The API provides a thread-safe logging function that prints to the R console:
```cpp
api->log("Something happened");
// Output: "unigd client: Something happened"
```
Safe to call from any thread.
## Memory management
Every function that returns a handle allocates memory that must be freed by the
corresponding destroy function. Failing to do so will leak memory.
| Allocator | Deallocator |
|--------------------------|----------------------------|
| `device_attach` | `device_destroy` |
| `device_render_create` | `device_render_destroy` |
| `device_plots_find` | `device_plots_find_destroy`|
| `renderers` | `renderers_destroy` |
| `renderers_find` | `renderers_find_destroy` |
The `unigd_render_access` buffer is owned by its `UNIGD_RENDER_HANDLE`. Copy
the data out if you need it after calling `device_render_destroy`.
Similarly, the `unigd_find_results.ids` array is owned by its
`UNIGD_FIND_HANDLE` and becomes invalid after `device_plots_find_destroy`.
## Thread safety
All API functions are safe to call from any thread. Internally, `unigd` uses a
shared mutex to synchronize access to the plot store.
The `state_change` callback is invoked from R's main thread (during graphics
engine operations). If your callback needs to communicate with background
threads, use appropriate synchronization.
## API reference
### General
| Function | Signature | Description |
|----------|-----------|-------------|
| `log` | `void (const char *)` | Print a message to the R console (thread-safe) |
| `info` | `const char *()` | Returns `"unigd "` |
### Client registration
| Function | Signature | Description |
|----------|-----------|-------------|
| `register_client_id` | `UNIGD_CLIENT_ID ()` | Obtain a unique client ID |
### Device operations
| Function | Signature | Description |
|----------|-----------|-------------|
| `device_attach` | `UNIGD_HANDLE (int devnum, unigd_graphics_client *, UNIGD_CLIENT_ID, void *)` | Attach client to device |
| `device_get` | `void *(int devnum, UNIGD_CLIENT_ID)` | Retrieve client data |
| `device_destroy` | `void (UNIGD_HANDLE)` | Free device handle |
| `device_state` | `unigd_device_state (UNIGD_HANDLE)` | Query device state |
### Plot history
| Function | Signature | Description |
|----------|-----------|-------------|
| `device_plots_find` | `UNIGD_FIND_HANDLE (UNIGD_HANDLE, UNIGD_PLOT_RELATIVE, UNIGD_PLOT_INDEX, unigd_find_results *)` | Query plot IDs |
| `device_plots_find_destroy` | `void (UNIGD_FIND_HANDLE)` | Free find results |
| `device_plots_remove` | `bool (UNIGD_HANDLE, UNIGD_PLOT_ID)` | Remove a plot |
| `device_plots_clear` | `bool (UNIGD_HANDLE)` | Clear all plots |
### Rendering
| Function | Signature | Description |
|----------|-----------|-------------|
| `device_render_create` | `UNIGD_RENDER_HANDLE (UNIGD_HANDLE, UNIGD_RENDERER_ID, UNIGD_PLOT_ID, unigd_render_args, unigd_render_access *)` | Render a plot |
| `device_render_destroy` | `void (UNIGD_RENDER_HANDLE)` | Free render data |
### Renderers
| Function | Signature | Description |
|----------|-----------|-------------|
| `renderers` | `UNIGD_RENDERERS_HANDLE (unigd_renderers_list *)` | List all renderers |
| `renderers_destroy` | `void (UNIGD_RENDERERS_HANDLE)` | Free renderer list |
| `renderers_find` | `UNIGD_RENDERERS_ENTRY_HANDLE (UNIGD_RENDERER_ID, unigd_renderer_info *)` | Look up a renderer |
| `renderers_find_destroy` | `void (UNIGD_RENDERERS_ENTRY_HANDLE)` | Free renderer lookup |