Skip to content

extism/c-pdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Extism C PDK

This project contains a tool that can be used to create Extism Plug-ins in C.

Installation

The Extism C PDK is a single header library. Just copy extism-pdk.h into your project or add this repo as a Git submodule:

git submodule add https://github.com/extism/c-pdk extism-pdk

Getting Started

The goal of writing an Extism plug-in is to compile your C code to a Wasm module with exported functions that the host application can invoke. The first thing you should understand is creating an export.

Exports

Let's write a simple program that exports a greet function which will take a name as a string and return a greeting string. Paste this into a file plugin.c:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>

#define Greet_Max_Input 1024
static const char Greeting[] = "Hello, ";

int32_t EXTISM_EXPORTED_FUNCTION(greet) {
  uint64_t inputLen = extism_input_length();
  if (inputLen > Greet_Max_Input) {
    inputLen = Greet_Max_Input;
  }

  // Load input
  static uint8_t inputData[Greet_Max_Input];
  extism_load_input(0, inputData, inputLen);

  // Allocate memory to store greeting and name
  const uint64_t greetingLen = sizeof(Greeting) - 1;
  const uint64_t outputLen = greetingLen + inputLen;
  ExtismHandle handle = extism_alloc(outputLen);
  extism_store_to_handle(handle, 0, Greeting, greetingLen);
  extism_store_to_handle(handle, greetingLen, inputData, inputLen);

  // Set output
  extism_output_set_from_handle(handle, 0, outputLen);
  return 0;
}

The EXTISM_EXPORTED_FUNCTION macro simplifies declaring an Extism function that will be exported to the host.

The load, store, and alloc functions are used to load from, store to and allocate Extism memory. Extism eases passing data to and from the host and the plug-in by managing memory isolated from both the host and the plug-in/Wasm module. For more details, see the Memory concept page.

Since we don't need any system access for this, we can compile this directly with clang:

clang -o plugin.wasm --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry plugin.c

The above command may fail if ran with system clang. It's highly recommended to use clang from the wasi-sdk instead. The wasi-sdk also includes a libc implementation targeting WASI, necessary for plugins that need the C standard library.

Let's break down the command a little:

  • --target=wasm32-unknown-unknown configures the correct Webassembly target
  • -nostdlib tells the compiler not to link the standard library
  • -Wl,--no-entry is a linker flag to tell the linker there is no _start function

We can now test plugin.wasm using the Extism CLI's call command:

extism call plugin.wasm greet --input="Benjamin"
# => Hello, Benjamin

More Exports: Error Handling

We catch any exceptions thrown and return them as errors to the host. Suppose we want to re-write our greeting module to never greet Benjamins:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#define Greet_Max_Input 1024
static const char Greeting[] = "Hello, ";

static bool is_benjamin(const char *name) {
  return strcasecmp(name, "benjamin") == 0;
}

int32_t EXTISM_EXPORTED_FUNCTION(greet) {
  uint64_t inputLen = extism_input_length();
  const uint64_t greetMaxString = Greet_Max_Input - 1;
  if (inputLen > greetMaxString) {
    inputLen = greetMaxString;
  }

  // Load input
  static uint8_t inputData[Greet_Max_Input];
  extism_load_input(0, inputData, inputLen);
  inputData[inputLen] = '\0';

  // Check if the input matches "benjamin", if it does
  // return an error
  if (is_benjamin((const char *)inputData)) {
    ExtismHandle err = extism_alloc_buf_from_sz("ERROR");
    extism_error_set(err);
    return -1;
  }

  // Allocate memory to store greeting and name
  const uint64_t greetingLen = sizeof(Greeting) - 1;
  const uint64_t outputLen = greetingLen + inputLen;
  ExtismHandle handle = extism_alloc(outputLen);
  extism_store_to_handle(handle, 0, Greeting, greetingLen);
  extism_store_to_handle(handle, greetingLen, inputData, inputLen);

  // Set output
  extism_output_set_from_handle(handle, 0, outputLen);
  return 0;
}

This time we will compile our example using wasi-sdk, since we used the <string.h> header file. And because we are targeting wasm32-wasi, we will need to add the -mexec-model=reactor flag to be able to export specific functions instead of a single _start function:

$WASI_SDK_PATH/bin/clang -o plugin.wasm plugin.c -mexec-model=reactor
extism call plugin.wasm greet --input="Benjamin" --wasi
# => Error: ERROR
echo $? # print last status code
# => 1
extism call plugin.wasm greet --input="Zach" --wasi
# => Hello, Zach
echo $?
# => 0

Configs

Configs are key-value pairs that can be passed in by the host when creating a plug-in. These can be useful to statically configure the plug-in with some data that exists across every function call. Here is a trivial example using extism_config_get:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>
#include <stdlib.h>

#define Greet_Max_Input 1024
static const char Greeting[] = "Hello, ";

int32_t EXTISM_EXPORTED_FUNCTION(greet) {
  ExtismHandle key = extism_alloc_buf_from_sz("user");
  ExtismHandle value = extism_config_get(key);

  if (value == 0) {
    ExtismHandle err = extism_alloc_buf_from_sz("Invalid key");
    extism_error_set(err);
    return -1;
  }

  const uint64_t valueLen = extism_length(value);

  // Load config value
  uint8_t *valueData = malloc(valueLen);
  if (valueData == NULL) {
    ExtismHandle err = extism_alloc_buf_from_sz("OOM");
    extism_error_set(err);
    return -1;
  }
  extism_load_from_handle(value, 0, valueData, valueLen);

  // Allocate memory to store greeting and name
  const uint64_t greetingLen = sizeof(Greeting) - 1;
  const uint64_t outputLen = greetingLen + valueLen;
  ExtismHandle handle = extism_alloc(outputLen);
  extism_store_to_handle(handle, 0, Greeting, greetingLen);
  extism_store_to_handle(handle, greetingLen, valueData, valueLen);
  free(valueData);

  // Set output
  extism_output_set_from_handle(handle, 0, outputLen);
  return 0;
}

To test it, the Extism CLI has a --config option that lets you pass in key=value pairs:

extism call plugin.wasm greet --config user=Benjamin
# => Hello, Benjamin

Variables

Variables are another key-value mechanism but it's a mutable data store that will persist across function calls. These variables will persist as long as the host has loaded and not freed the plug-in. You can use extism_var_get, and extism_var_set to manipulate vars:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>

int32_t EXTISM_EXPORTED_FUNCTION(count) {
  ExtismHandle key = extism_alloc_buf_from_sz("count");
  ExtismHandle value = extism_var_get(key);

  uint64_t count = 0;
  if (value != 0) {
    extism_load_from_handle(value, 0, &count, sizeof(uint64_t));
  }
  count += 1;

  // Allocate a new value if it isn't saved yet
  if (value == 0) {
    value = extism_alloc(sizeof(uint64_t));
  }

  // Update the memory block
  extism_store_to_handle(value, 0, &count, sizeof(uint64_t));

  // Set the variable
  extism_var_set(key, value);

  return 0;
}

Logging

The extism_log* functions can be used to emit logs:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>

int32_t EXTISM_EXPORTED_FUNCTION(log_stuff) {
  ExtismHandle msg = extism_alloc_buf_from_sz("Hello!");
  extism_log_info(msg);
  extism_log_debug(msg);
  extism_log_warn(msg);
  extism_log_error(msg);
  extism_log_sz("Hello!", ExtismLogInfo);
  return 0;
}

Running it, you need to pass a log-level flag:

extism call plugin.wasm log_stuff --log-level=info
# => 2023/10/17 14:25:00 Hello!
# => 2023/10/17 14:25:00 Hello!
# => 2023/10/17 14:25:00 Hello!
# => 2023/10/17 14:25:00 Hello!
# => 2023/10/17 14:25:00 Hello!
# => 2023/10/17 14:25:00 Hello!

HTTP

HTTP calls can be made using extism_http_request:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>
#include <string.h>

int32_t EXTISM_EXPORTED_FUNCTION(call_http) {
  const char *reqStr = "{\
    \"method\": \"GET\",\
    \"url\": \"https://jsonplaceholder.typicode.com/todos/1\"\
  }";

  ExtismHandle req = extism_alloc_buf_from_sz(reqStr);
  ExtismHandle res = extism_http_request(req, 0);

  if (extism_http_status_code() != 200) {
    return -1;
  }

  extism_output_set_from_handle(res, 0, extism_length(res));
  return 0;
}

To test it you will need to pass --allow-host jsonplaceholder.typicode.com to the extism CLI, otherwise the HTTP request will be rejected.

Imports (Host Functions)

Like any other code module, Wasm not only let's you export functions to the outside world, you can import them too. Host Functions allow a plug-in to import functions defined in the host. For example, if you host application is written in Python, it can pass a Python function down to your C plug-in where you can invoke it.

This topic can get fairly complicated and we have not yet fully abstracted the Wasm knowledge you need to do this correctly. So we recommend reading out concept doc on Host Functions before you get started.

A Simple Example

Host functions have a similar interface as exports. You just need to declare them as extern on the top of your header file. You only declare the interface as it is the host's responsibility to provide the implementation:

extern ExtismHandle a_python_func(ExtismHandle);

A namespace may be set for an import using the IMPORT macro in extism-pdk.h:

IMPORT("my_module", "a_python_func") extern ExtismHandle a_python_func(ExtismHandle);

Note: The types we accept here are the same as the exports as the interface also uses the convert crate.

To call this function, we pass an Extism handle and receive one back:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"
#include <stdint.h>

int32_t EXTISM_EXPORTED_FUNCTION(hello_from_python) {
  ExtismHandle arg = extism_alloc_buf_from_sz("Hello!");
  ExtismHandle res = a_python_func(arg);
  extism_free(arg);
  extism_output_set_from_handle(res, 0, extism_length(res));
  return 0;
}

Testing it out

We can't really test this from the Extism CLI as something must provide the implementation. So let's write out the Python side here. Check out the docs for Host SDKs to implement a host function in a language of your choice.

from extism import host_fn, Plugin

@host_fn()
def a_python_func(input: str) -> str:
    # just printing this out to prove we're in Python land
    print("Hello from Python!")

    # let's just add "!" to the input string
    # but you could imagine here we could add some
    # applicaiton code like query or manipulate the database
    # or our application APIs
    return input + "!"

Now when we load the plug-in we pass the host function:

manifest = {"wasm": [{"path": "/path/to/plugin.wasm"}]}
plugin = Plugin(manifest, functions=[a_python_func], wasi=True)
result = plugin.call('hello_from_python', b'').decode('utf-8')
print(result)
python3 app.py
# => Hello from Python!
# => An argument to send to Python!

Building

One source file must contain the implementation:

#define EXTISM_IMPLEMENTATION
#include "extism-pdk.h"

All other source files using the pdk must include the header without #define EXTISM_IMPLEMENTATION

The C PDK does not require building with libc, but additional functions can be enabled when libc is available. #define EXTISM_USE_LIBC in each file before including the pdk (everywhere it is included) or, when compiling, pass it as a flag to clang: -D EXTISM_USE_LIBC

The low-level API that operates on ExtismPointer is no longer included by default, #define EXTISM_ENABLE_LOW_LEVEL_API in each file before including the pdk (everywhere it is included) or, when compiling, pass it as a flag to clang: -D EXTISM_ENABLE_LOW_LEVEL_API . Updating to use the ExtismHandle-based API is highly recommended.

The C PDK may be used from C++, however, the implementation must be built with a C compiler. See cplusplus in tests/Makefile for an example.

Exports (details)

The EXTISM_EXPORTED_FUNCTION macro is not essential to create a plugin function and export it to the host. You may instead write a function and then export it when linking. For example, the first example may have the following signature instead:

int32_t greet(void)

Then, it can be built and linked with:

$WASI_SDK_PATH/bin/clang -o plugin.wasm --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry -Wl,--export=greet plugin.c

Note the -Wl,--export=greet

Exports names do not necessarily have to match the function name either. Going back to the first example again. Try:

EXTISM_EXPORT_AS("greet") int32_t internal_name_for_greet(void)

and build with:

$WASI_SDK_PATH/bin/clang -o plugin.wasm --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry plugin.c

Reach Out!

Have a question or just want to drop in and say hi? Hop on the Discord!