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.
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
.
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.
smartview.expose("add_ten", [](int i)
{
std::this_thread::sleep_for(std::chrono::seconds(10));
return i + 10;
}, saucer::launch::async);
There is no exception handling for exposed functions. You need to make sure to handle exceptions yourself otherwise things will break!
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
.
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.
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);
Since version 5.0.0
saucer also supports using std::expected
for promise rejection.
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.
const result = await saucer.call('add_ten', [10]);
// > result == 20
Since version 3.0.0
saucer also supports the following syntax to call exposed functions:
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});
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();
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.
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)
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::future
s and want to wait until all of them are ready you can use saucer::all
.
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.