blob: a617f5616ca972cec0ef0bd381a31866897db3aa [file] [log] [blame] [view]
---
permalink: /docs/builtin-functions
---
# How to Add Built-in Functions
There are several ways to execute user-defined functions in the Teaclave
platform. One simple way is to write Python scripts and register them as functions,
and the scripts will be executed by the *MesaPy executor*. Another way is to add native
functions as built-in functions, and they will be managed by the *Built-in executor*.
Compared to Python scripts, native built-in functions implemented in Rust are
memory-safe, have better performance, support more third-party libraries and
can be remotely attested as well. In this document, we will guide you through
how to add a built-in function to Teaclave step by step with a "private join and
compute" example.
In this example, consider several banks have names and balance of their clients.
These banks want to compute the total balance of common clients in their private
data set without leaking the raw sensitive data to other parties. This is
a perfect usage scenario of the Teaclave platform, and we will provide a
solution by implementing a built-in function in Teaclave.
## Implement Built-in Functions in Rust
All built-in functions are implemented in the `teaclave_function` crate and can
be selectively compiled using feature gates. Basically, one built-in function
needs two things: a name and a function implementation. Follow the convention of
other built-in function implementations, we define our "private join and
compute" function like this:
```rust
#[derive(Default)]
pub struct PrivateJoinAndCompute;
impl PrivateJoinAndCompute {
pub const NAME: &'static str = "builtin-private-join-and-compute";
pub fn new() -> Self {
Default::default()
}
pub fn run(
&self,
arguments: FunctionArguments,
runtime: FunctionRuntime,
) -> Result<String> {
...
Ok(summary)
}
```
The `NAME` is the identifier of a function, which is used for creating tasks.
Usually, the name of a built-in function starts with the `built-in` prefix. In
addition, we need to define an entry point of the function, which is the `run`
function. The `run` function can take arguments (in the `FunctionAruguments`
type) and runtime (in the `FunctionRuntime` type) for interacting with external
resources (e.g., reading/writing input/output files). Also, the `run` function
can return a summary of the function execution.
Since the function arguments is in the JSON object format and can be easily
deserialized to a Rust struct with `serde_json`. Therefore, we define a struct
`PrivateJoinAndComputeArguments` which derive the `serde::Deserialize` trait for
the conversion. Then we implement `TryFrom` trait for the struct to convert the
`FunctionArguments` type to the actual `PrivateJoinAndComputeArguments` type.
```rust
#[derive(serde::Deserialize)]
struct PrivateJoinAndComputeArguments {
num_user: usize, // Number of users in the multiple party computation
}
impl TryFrom<FunctionArguments> for PrivateJoinAndComputeArguments {
type Error = anyhow::Error;
fn try_from(arguments: FunctionArguments) -> Result<Self, Self::Error> {
use anyhow::Context;
serde_json::from_str(&arguments.into_string()).context("Cannot deserialize arguments")
}
}
```
When executing the function, a `runtime` object will be passed to the function.
We can read or write files with the `runtime` with the `open_input` and
`create_output` functions.
```rust
// Read data from a file
let mut input_io = runtime.open_input(&input_file_name)?;
input_io.read_to_end(&mut data)?;
...
// Write data into a file
let mut output = runtime.create_output(&output_file_name)?;
output.write_all(&output_bytes)?;
```
## Register Functions in the Executor
To use the function, we need to register it to the built-in executor. Please also
put a `cfg` attribute to make sure developers can conditionally build functions
into the executor.
``` rust
impl TeaclaveExecutor for BuiltinFunctionExecutor {
fn execute(
&self,
name: String,
arguments: FunctionArguments,
_payload: String,
runtime: FunctionRuntime,
) -> Result<String> {
match name.as_str() {
...
#[cfg(feature = "builtin_private_join_and_compute")]
PrivateJoinAndCompute::NAME => PrivateJoinAndCompute::new().run(arguments, runtime),
...
}
}
}
```
## Invoke Functions with the Client SDK
Finally, we can invoke the function with the client SDK. In our example, we use
the Python client SDK. Basically, this process includes registering input/output
files, creating tasks, approving tasks, invoking tasks and getting execution
results. You can see more details in the `examples/python` directory.