Creating an Extension Quickstart

Note

See Extension API Reference, Widget Client Protocol, and the sample extension win_simple_perf for more details.

Getting Started

Quasar implements a WebSocket-based Data Server that facilitates the communication of various data that isn’t available in a web-only context to widgets. The Data Server can be extended by extensions that provide additional Data Sources that can be used by widgets. The Extension API is provided as a pure-C interface, so extensions can be written in languages that support implementing C interfaces and produces a native library. This guide assumes that the language of choice is C++.

To begin, your extensions must include extension_api.h (which includes extension_types.h) as well as extension_support.h.

At minimum, an extension needs to implement 3 functions in additional to quasar_ext_load() and quasar_ext_destroy().

These are:

  • bool init(quasar_ext_handle handle)

  • bool shutdown(quasar_ext_handle handle)

  • bool get_data(size_t uid, quasar_data_handle dataHandle, char* args)

Within the quasar_ext_info_t structure.

For each Data Dource provided by the extension, a quasar_data_source_t entry needs to be created that contains the Data Source’s identifier and refresh rate in microseconds or polling style. quasar_data_source_t::validtime specifies the amount of time in milliseconds that the data is cached and remains valid for, for sources using the Client Polling style. quasar_data_source_t::uid should be initialized to 0.

The entries should be propagated in quasar_ext_info_t::numDataSources and quasar_ext_info_t::dataSources.

The rest of the static data fields in quasar_ext_info_t::fields such as quasar_ext_info_fields_t::name, quasar_ext_info_fields_t::fullname, and quasar_ext_info_fields_t::version should be filled in with the extension’s basic information.

Example

Adapted from the sample extension win_simple_perf:

quasar_data_source_t sources[] = {
    {       "sysinfo",               5000000,    0, 0},
    {"sysinfo_polled", QUASAR_POLLING_CLIENT, 1000, 0}
};

quasar_ext_info_fields_t fields =
    {
        "win_simple_perf",                                      // char name[16]
        "Simple Performance Query",                             // char fullname[64]
        "3.0",                                                  // char version[64]
        "r52",                                                  // char author[64]
        "Provides basic PC performance metrics",                // char description[256]
        "https://github.com/r52/quasar"                         // char url[256]
    };

quasar_ext_info_t info =
    {
        QUASAR_API_VERSION,                                     // int api_version, should always be QUASAR_API_VERSION
        &fields,                                                // quasar_ext_info_fields_t* fields. Must be initialized

        std::size(sources),                                     // size_t numDataSources
        sources,                                                // quasar_data_source_t* dataSources

        simple_perf_init,                                       // bool init(quasar_ext_handle handle)
        simple_perf_shutdown,                                   // bool shutdown(quasar_ext_handle handle)
        simple_perf_get_data,                                   // bool get_data(size_t uid, quasar_data_handle dataHandle, char* args)
        nullptr,                                                // quasar_settings_t* create_settings(quasar_ext_handle handle)
        nullptr                                                 // void update(quasar_settings_t* settings)
    };

In this example, 2 Data Sources are defined, sysinfo and sysinfo_polled, where sysinfo uses the subscription model, while sysinfo_polled uses the Client Polling model. The functions simple_perf_init(), simple_perf_shutdown(), and simple_perf_get_data() are the implementations of init(), shutdown(), and get_data() respectively. Note that create_settings() and update() are not implemented by this extension. These functions are optional, and only needs to be implemented if the extension provides custom settings. See Custom Settings for more information.

quasar_ext_load()

This function should return a pointer to a populated quasar_ext_info_t structure.

Following previous example:

quasar_ext_info_t* quasar_ext_load(void)
{
    return &info;
}

Since the quasar_ext_info_t info structure is defined statically in the previous example, it is suffice for quasar_ext_load() to simply return the pointer to it.

quasar_ext_destroy()

This function should deallocate anything that was allocated for the quasar_ext_info_t structure.

Following previous examples:

void quasar_ext_destroy(quasar_ext_info_t* info)
{
    // does nothing; info is on stack
    return;
}

Since both the quasar_data_source_t sources as well as the quasar_ext_info_t info structure and all of its contents are defined statically in the previous examples, we do not need to deallocate anything for the destruction of the quasar_ext_info_t structure. Therefore, the function does nothing.

init()

If the extension was loaded successfully, each Data Source entry’s quasar_data_source_t::uid is filled with a unique identifier. These are used in the get_data() function call to identify the Data Source being requested. It is up to the extension to remember these during init() as they will be referred to by future get_data() calls from Quasar.

This function should also allocate or initialize any other resources needed, as well as remember the extension handle if necessary.

bool simple_perf_init(quasar_ext_handle handle)
{
    extHandle = handle;

    // Process uid entries.
    if (sources[0].uid == 0)
    {
        // "sysinfo" Data Source didn't get a uid
        return false;
    }

    if (sources[1].uid == 0)
    {
        // "sysinfo_polled" Data Source didn't get a uid
        return false;
    }

    return true;
}

shutdown()

This function should deallocate and clean up any resources allocated in init(), including waiting on any threads spawned. Since we have no allocations in our sample init() function, our shutdown() can simply return.

bool simple_perf_shutdown(quasar_ext_handle handle)
{
    return true;
}

get_data()

This function is responsible for retrieving the data requested by the uid argument and populating it into the quasar_data_handle handle using functions from extension_support.h.

Note

This function needs to be both re-entrant and thread-safe!

bool simple_perf_get_data(size_t uid, quasar_data_handle hData, char* args)
{
    if (srcUid != sources[0].uid && srcUid != sources[1].uid)
    {
        warn("Unknown source {}", srcUid);
        return false;
    }

    // CPU data
    double cpu = GetCPULoad() * 100.0;

    // https://stackoverflow.com/questions/63166/how-to-determine-cpu-and-memory-consumption-from-inside-a-process
    // RAM data
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(MEMORYSTATUSEX);
    GlobalMemoryStatusEx(&memInfo);
    DWORDLONG totalPhysMem = memInfo.ullTotalPhys;
    DWORDLONG physMemUsed  = memInfo.ullTotalPhys - memInfo.ullAvailPhys;

    auto      res          = fmt::format("{{\"cpu\":{},\"ram\":{{\"total\":{},\"used\":{}}}}}", (int) cpu, totalPhysMem, physMemUsed);

    quasar_set_data_json(hData, res.c_str());

    return true;
}

See extension_support.h and extension_support.hpp for all supported data types.

Data Models

Quasar supports three different types of data models for Data Sources.

By default, Data Sources in Quasar operate on a timer-based subscription model.

This can be changed by initializing quasar_data_source_t::rate of a Data Source entry to different values. A positive value means the default timer-based subscription. A value of QUASAR_POLLING_CLIENT means the client widget is responsible for polling the extension for new data. A value of QUASAR_POLLING_SIGNALED means the extension will signal when new data becomes available (i.e. from a thread) and automatically send the new data to all subscribed widgets.

See Widget Client Protocol for details on client message formats.

Timer-based Subscription

Enabled by initializing quasar_data_source_t::rate of a Data Source entry to a positive value.

Multiple client widgets may subscribe to a single data source, which is polled for new data every quasar_data_source_t::rate microseconds. This new data is then propagated to every subscribed widget.

Signal-based Subscription

Enabled by initializing quasar_data_source_t::rate to QUASAR_POLLING_SIGNALED.

This model supports Data Sources which require inconsistent timing, as well as Data Sources which require background processing, such as a producer-consumer thread.

To use this model, utilize the functions quasar_signal_data_ready() and quasar_signal_wait_processed() in extension_support.h.

For example:

quasar_data_source_t sources[2] =
    {
      { "some_thread_source", QUASAR_POLLING_SIGNALED, 0, 0 },
      { "some_timer_source", 5000000, 0, 0 }
    };

quasar_ext_handle extHandle = nullptr;
std::atomic_bool running = true;
std::thread workThd;

void workerThread()
{
    while (running)
    {
        // do the work
        ...

        // signal that data is ready
        quasar_signal_data_ready(extHandle, "some_thread_source");

        // call this function if the thread needs to wait for the data to be consumed
        // before processing new data
        quasar_signal_wait_processed(extHandle, "some_thread_source");
    }
}

bool init_func(quasar_ext_handle handle)
{
    extHandle = handle;

    // start the worker thread
    workThd = std::thread{workerThread};

    return true;
}

bool shutdown_func(quasar_ext_handle handle)
{
    running = false;

    // join the worker thread
    workThd.join();

    return true;
}

Client Polling

Enabled by initializing quasar_data_source_t::rate to QUASAR_POLLING_CLIENT.

This data model transfers the responsibility of polling for new data to the client widget. The data source does not accept subscribers.

Example:

quasar_data_source_t sources[2] =
    {
      { "some_polled_source", QUASAR_POLLING_CLIENT, 1000, 0 },
      { "some_timer_source", 5000000, 0, 0 }
    };

From the client:

function poll() {
    const reg = {
        method: "query",
        params: {
            topics: ["some_extension/some_polled_source"]
        }
    };

    websocket.send(JSON.stringify(reg));
}

In this example, quasar_data_source_t::validtime is configured with a value of 1000ms. This is the time that the data returned by some_polled_source is cached for after retrieval. Any polls to some_polled_source within the time duration will return the cached data.

This model also allows the extension to signal data ready using quasar_signal_data_ready() for an asynchronous poll request/response timing.

The sample code in the above sections are based on this model.

Custom Settings

By default, users can enable or disable a Data Source as well as change its refresh rate from the Settings dialog.

However, a extension can provide further custom settings by utilizing the extension_support.h API and implementing the create_settings() and update() functions in quasar_ext_info_t. These custom settings will appear under the Settings dialog.

Sample code:

quasar_settings_t* create_custom_settings(quasar_ext_handle handle)
{
    quasar_settings_t* settings = quasar_create_settings(handle);
    quasar_add_bool_setting(handle, settings, "s_levelenabled", "Process Level", true);
    quasar_add_int_setting(handle, settings, "s_level", "Level", 1, 30, 1, 1);

    return settings;
}

void custom_settings_update(quasar_settings_t* settings)
{
    g_levelenabled = quasar_get_bool_setting(extHandle, settings, "s_levelenabled");
    g_level = quasar_get_int_setting(extHandle, settings, "s_level");
}