diff --git a/README.md b/README.md index e5178f8..153e654 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 + +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 +#include +#include -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 + +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> kvStore; + + auto t = std::vector{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 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 @@ -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 +```