Interoperability
In the previous chapter, you've created your first saucer application.
Without diving into the library headers, you've probably not noticed, that saucer::smartview
has a defaulted template parameter Serializer
.
I'll now go into more detail on how exactly Interoperability works and will briefly talk about what a serializer is.
What is a Serializer
In short, a serializer is something that transforms native types into something that both JavaScript and C++ understand.
Saucer currently ships a default serializer that is based on glaze, however you're free to implement your own if desired (see Custom Serializers).
What is a smartview
A smartview
is a type of webview
that makes use of a serializer to exchange data between C++ and JavaScript.
For more information see:
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 specify the amount of threads used in the saucer::preferences
taken by the smartview constructor.
saucer::smartview{{.threads = 1}};
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);
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 need to capture 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;
});
Forget
This function is deprecated since saucer v4.2.0!
In case you don't care about the result of an evaluation, use execute instead.
smartview.execute("Math.random()");
smartview.execute("Math.pow({}, {})", 2, 5);
Use saucer::forget
in case you want to discard (not use) the result of the evaluation.
saucer::forget(smartview.evaluate<float>("Math.random()"));
smartview.evaluate<float>("Math.random()") | saucer::forget();
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.