Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add getting started #13

Merged
merged 4 commits into from
Dec 6, 2023
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 177 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Extism cpp-sdk

C++ Host SDK for Extism
The C++ SDK for integrating with the [Extism](https://extism.org/) runtime. Add this library in your host C++ applications to run Extism plugins.

## Getting Started
Join the [Extism Discord](https://extism.org/discord) and chat with us!

## Building and Installation

### Install Dependencies

Expand All @@ -14,30 +16,188 @@ C++ Host SDK for Extism
If you wish to do link jsoncpp statically, or do an in-tree build, See
[Alternative Dependency Strategies](#Alternative-Dependency-Strategies).

### Building
### Build and Install cpp-sdk

```bash
cmake -B build
cmake --build build -j
sudo cmake --install build
```

### Testing
## Getting Started

After building, run the following from the build directory:
To add the cpp-sdk to a CMake project:

```bash
make test
```cmake
find_package(extism-cpp)
target_link_libraries(getting-started extism-cpp)
```

### Loading a Plug-in

The primary concept in Extism is the [plug-in](https://extism.org/docs/concepts/plug-in). A plug-in is a code module stored in a `.wasm` file.

Plug-in code can come from a file on disk, object storage or any number of places. Since you may not have one handy, let's load a demo plug-in from the web. Let's
start by creating a main function that loads a plug-in:

```cpp
#include <extism.hpp>

int main(void) {
const auto manifest =
extism::Manifest::wasmURL("https://github.com/extism/plugins/releases/"
"latest/download/count_vowels.wasm");
extism::Plugin plugin(manifest, true);
}
```

### Installation
### Calling A Plug-in's Exports

This plug-in was written in Rust and it does one thing, it counts vowels in a string. It exposes one "export" function: `count_vowels`. We can call exports using `Plugin::call`.
Let's add code to call `count_vowels` to our main func:

```c++
#include <extism.hpp>
#include <iostream>
#include <string>

After building, run the following from the build directory:
int main(void) {
// ...

const std::string hello("Hello, World!");
auto out = plugin.call("count_vowels", hello);
std::string response(out.string());
std::cout << response << std::endl;
// => {"count":3,"total":3,"vowels":"aeiouAEIOU"}
}
```

Build it:

```bash
sudo make install
cmake -B build && cmake --build build
```

Running this should print out the JSON vowel count report:

```shell
./build/getting-started
{"count":3,"total":3,"vowels":"aeiouAEIOU"}
```

All exports have the same interface, optional bytes in and optional bytes out. This plug-in happens to take a string and return a JSON encoded string with a report of results.

### Plug-in State

Plug-ins may be stateful or stateless. Plug-ins can maintain state between calls using variables. Our count vowels plug-in remembers the total number of counted vowels in the "total" key in the result. You can see this by making subsequent calls to the export:

```cpp
const std::string hello("Hello, World!");
auto out = plugin.call("count_vowels", hello);
std::string response(out.string());
std::cout << response << std::endl;
// => {"count":3,"total":3,"vowels":"aeiouAEIOU"}

out = plugin.call("count_vowels", hello);
response = out.string();
std::cout << response << std::endl;
// => {"count":3,"total":6,"vowels":"aeiouAEIOU"}
```

The state variables will persist until the plug-in is freed or reinitialized.

### Configuration

Plug-ins may optionally take a configuration object. This is a static way to configure the plug-in. Our count-vowels plugin takes an optional configuration to change out which characters are considered vowels. Example:

```cpp
// #include ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth just using #include <extism.h> here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to #include <extism.hpp>


int main(void) {
auto manifest =
extism::Manifest::wasmURL("https://github.com/extism/plugins/releases/"
"latest/download/count_vowels.wasm");
manifest.setConfig("vowels", "aeiouyAEIOUY");
extism::Plugin plugin(manifest, true);

const std::string hello("Yellow, World!");
auto out = plugin.call("count_vowels", hello);
std::string response(out.string());
std::cout << response << std::endl;
// => {"count":4,"total":4,"vowels":"aeiouyAEIOUY"}
}
```

### Linking

### Host Functions

Let's extend our count-vowels example a little bit: Instead of storing the `total` in an ephemeral plug-in var, let's store it in a persistent key-value store!

Wasm can't use our KV store on it's own. This is where [Host Functions](https://extism.org/docs/concepts/host-functions) come in.

[Host functions](https://extism.org/docs/concepts/host-functions) allow us to grant new capabilities to our plug-ins from our application. In this case, they are native C++ functions that are passed to and can can be invoked by the plug-in.

Let's load the manifest like usual but load up the `count_vowels_kvstore` plug-in:

```cpp
const auto manifest =
extism::Manifest::wasmURL("https://github.com/extism/plugins/releases/"
"latest/download/count_vowels_kvstore.wasm");
```

> *Note*: The source code for this is [here](https://github.com/extism/plugins/blob/main/count_vowels_kvstore/src/lib.rs) and is written in rust, but it could be written in any of our PDK languages.

Unlike our previous plug-in, this plug-in expects you to provide host functions that satisfy its import interface for a KV store.

We want to expose two functions to our plugin, `kv_write` which writes a bytes value to a key and `kv_read` which reads the bytes at the given key:

```cpp
// pretend this is Redis or something :)
std::map<std::string, std::vector<uint8_t>> kvStore;

auto t = std::vector<extism::ValType>{extism::ValType::I64};
auto kvRead = extism::Function(
"kv_read", t, t,
[&kvStore](extism::CurrentPlugin plugin, void *user_data) {
const auto it = kvStore.find(plugin.inputString());
if (it == kvStore.end()) {
const std::vector<uint8_t> zeros{0, 0, 0, 0};
plugin.output(zeros.data(), zeros.size());
return;
}
plugin.output(it->second.data(), it->second.size());
});
auto kvWrite = extism::Function(
"kv_write", {extism::ValType::I64, extism::ValType::I64}, {},
[&kvStore](extism::CurrentPlugin plugin, void *user_data) {
const auto key = plugin.inputString(0);
auto value = plugin.inputBuffer(1);
kvStore[key] = value.vector();
});
```

We need to pass these imports to the plug-in to create them. All imports of a plug-in must be satisfied for it to be initialized:

```cpp
extism::Plugin plugin(manifest, true, {kvRead, kvWrite});
```

Now, we can demo it:

```cpp
const std::string hello("Hello, World!");
auto out = plugin.call("count_vowels", hello);
std::string response(out.string());
std::cout << response << std::endl;
// => {"count":3,"total":3,"vowels":"aeiouAEIOU"}

out = plugin.call("count_vowels", hello);
response = out.string();
std::cout << response << std::endl;
// => {"count":3,"total":6,"vowels":"aeiouAEIOU"}
```

## Linking

#### CMake

Expand Down Expand Up @@ -104,3 +264,9 @@ cmake -DEXTISM_CPP_BUILD_IN_TREE=1 -B build && cmake --build build
If `EXTISM_CPP_BUILD_IN_TREE` is not set and `find_package` fails, the dependency is fetched from the internet using `FetchContent` and built from source.

The author believes this is the worst way to deal with the deps as it requires building them from source and doesn't use a centrally managed, flat tree of dependencies. It's provided just as a cross-platform convenience.

## Testing

```bash
cmake --build build --target test
```
Loading