Skip to main content

Interoperability

In this chapter we will look into how Interoperability (i.e. the expose/evaluate interface of the smartview) works.

What is a Serializer

Without diving into the library headers you may not have noticed that saucer::smartview has a defaulted template parameter Serializer.

A serializer is the core component that allows proper communication with the JavaScript world.

Saucer currently ships with two JSON based serializers out of the box, one based on glaze and the other based on reflect-cpp. The former being the default. To change the defaul serializer see: CMake Configuration.

You are not tied to the default serializers saucer provides. It is possible to write a custom serializer if desired.

note

To exchange data between C++ and JavaScript you will need to use a saucer::smartview - a saucer::webview will not suffice.

Exposing Functions

You can expose a native function to the JavaScript world by calling expose on your smartview.

Example: Exposing a function
smartview.expose("add_ten", [](int i)
{
return i + 10;
});

All exposed functions are called synchronously by default.
To make the call to your function asynchronous, simply pass a launch policy as the last parameter.

Example: Exposing a function asynchronously
smartview.expose("add_ten", [](int i)
{
std::this_thread::sleep_for(std::chrono::seconds(10));
return i + 10;
}, saucer::launch::async);
danger

There is no exception handling for exposed functions. You need to make sure to handle exceptions yourself otherwise things will break!

note

All async methods are executed by saucers internal thread-pool.
By default the thread-pool size is determined by a call to std::thread::hardware_concurrency().

You can customize the amount of threads used in the saucer::options passed to saucer::application::init.

Example: Custom Thread Count
auto app = saucer::application::init({ .id = "...", .threads = 10 });

Executors

All calls to the exposed functions are resolved by a promise.
If you wish to manually reject or resolve these, you can take a saucer::executor as the last argument of your callback.

Example: Taking executor to manually resolve promise
smartview.expose("add_ten", [](int i, const saucer::executor<int>& exec)
{
const auto& [resolve, reject] = exec;

if (i < 0)
{
return reject("Value should be >=0");
}

std::this_thread::sleep_for(std::chrono::seconds(10));
resolve(i + 10);
}, saucer::launch::async);
tip

Since version 5.0.0 saucer also supports using std::expected for promise rejection.

Example: Using std::expected to resolve promise
smartview.expose("add_ten", [](int i) -> std::expected<int, std::string> 
{
if (i < 0)
{
return std::unexpected{"Value should be >=0"};
}

return i + 10;
}, saucer::launch::async);

Invoke from JavaScript

Once you've exposed your function from the C++ side, you can call it from JavaScript.

Example: Calling your exposed function from the JavaScript World
const result = await saucer.call('add_ten', [10]);
// > result == 20
tip

Since version 3.0.0 saucer also supports the following syntax to call exposed functions:

Example: Calling your exposed function from the JavaScript World (New)
const result = await saucer.exposed.add_ten(10);
// > result == 20

Calling JavaScript

You can also execute JavaScript code and capture it's result using the evaluate method. In case you don't care about the result, use execute instead.

auto random = smartview.evaluate<float>("Math.random()").get();

You can also pass C++ objects as parameters when calling evaluate/execute.

auto random = smartview.evaluate<float>("Math.pow({}, {})", 2, 5).get();
smartview.execute("console.log({})", std::vector<int>{10});
tip

Instead of manually typing out the parameters you can also utilize saucer::make_args.

auto random = smartview.evaluate<float>("Math.pow({})", saucer::make_args(2, 5)).get();
caution

evaluate returns a std::future, which means that calling it outside of an asynchronous context will cause a deadlock!
To circumvent this take a look at the Future Utilities.

Future Utilities

Due to the aforementioned problems with using evaluate outside of asynchronous contexts, I've created some utility functions to make your life easier.

caution

All of the utilities provided here spawn a new thread and do not use any thread-pool.
If you wish to use a thread pool you should roll your own.

Start off by including the utility header:

#include <saucer/utils/future.hpp>

Then

saucer::then is a basic implementation for std::future::then (Which does not currently exist in the standard)

Example Usage
saucer::then(smartview.evaluate<float>("Math.random()"), [](float result)
{
std::cout << "The random number was " << result << std::endl;
});

smartview.evaluate<float>("Math.random()") | saucer::then([](float result)
{
std::cout << "Result: " << result << std::endl;
});

All

In case you have multiple std::futures and want to wait until all of them are ready you can use saucer::all.

Example Usage
auto a = smartview.evaluate<float>("Math.random()");
auto b = smartview.evaluate<float>("Math.random()");
auto c = smartview.evaluate<float>("Math.random()");

auto [random, random2, random3] = saucer::all(a, b, c);

User Defined Types

glaze supports the automatic serialization of aggregates, primitives as well as many STL types by default.
Please refer to their documentation on how to add support for third-party types.