hotg-ai / rune_vm

Apache License 2.0
1 stars 1 forks source link

Overview

C++ SDK for running Runes.

Table of Contents

Build

Tested on macOS Big Sur 11.1.

Prerequisites

Android

iOS

Guide

Prepare repo

Cloning submodules might take some time as they fetch a lot of redundant data for things like tests.

git clone git@github.com:hotg-ai/rune_vm.git
cd rune_vm
git submodule update --init --recursive

Configure via cmake

Common options

Notable cmake options - for complete list use cmake gui app:

Mac or other host system

PROJECT_DIR=$(pwd)
CONFIG_POSTFIX=rel
BUILD_DIR=build-$CONFIG_POSTFIX
mkdir $BUILD_DIR && cd $BUILD_DIR
cmake ../ \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX=$PROJECT_DIR/install-$CONFIG_POSTFIX

Android

NOTE: Tests are not supported on Android.

For details see https://developer.android.com/ndk/guides/cmake .

Please pass path to NDK on your system to NDK_PATH variable in the script below. NDK version might vary based on availability in your environment.

PROJECT_DIR=$(pwd)
INSTALL_DIR=$(pwd)
ABI=arm64-v8a
CONFIG_POSTFIX=android-rel-$ABI
BUILD_DIR=build-$CONFIG_POSTFIX
mkdir -p $BUILD_DIR && cd $BUILD_DIR
NDK_PATH=/path_to_android_sdk_on_you_machine/ndk/21.1.6352462
cmake $PROJECT_DIR \
    -DTFLITE_C_BUILD_SHARED_LIBS=ON \
    -DRUNE_VM_BUILD_TESTS=OFF \
    -DRUNE_VM_BUILD_EXAMPLES=OFF \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR/install-$CONFIG_POSTFIX \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_ARM_NEON=ON \
    -DANDROID_NDK=$NDK_PATH \
    -DANDROID_NATIVE_API_LEVEL=28 \
    -DANDROID_STL=c++_shared \
    -DANDROID_TOOLCHAIN=clang \
    -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake

iOS

NOTE: Tests are not supported on iOS.

If you use polly' toolchain, next env variables must be set:

Build

Notes:

  1. Tensorflow compilation via console tries to print pages for some reason. My console app - iTerm2 - blocks it after couple of requests, so if yours doesn't you may want to try it;
  2. If BUILD_WORKERS_COUNT doesn't work on your platform, you may pass your count of cores or omit --parallel arg altogether;
    BUILD_WORKERS_COUNT=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null)
    cmake --build ./ --target rune_vm --config Release --parallel $BUILD_WORKERS_COUNT

Install

cmake --build ./ --target install --config Release --parallel $BUILD_WORKERS_COUNT

Test

NOTE: RUNE_VM_BUILD_TESTS must be enabled during cmake configuration for tests to work.

./tests/rune_vm_tests

Integration

Generally all you to do to use it is to link the rune_vm in your project. There are multiple options to do so:

Caveats:

iOS recommended flow

Android recommended flow

User guide

Main Rune abstractions

Library abstractions

Examples

The easiest way to understand the interface is to checkout examples. They are located in the rune_vm/examples directory.

How to use

Implement the logger

struct StdoutLogger : public rune_vm::ILogger {
    void log(
        const rune_vm::Severity severity,
        const std::string& module,
        const std::string& message) const noexcept final {
        try {
            std::cout << std::string(rune_vm::severityToString(severity)) + "@[" + module + "]: " + message +"\n";
        } catch(...) {
            // recover somehow if you want
        }
    }
};

Implement the delegates

You must provide the delegate for each capability your Rune needs. You might implement multiple capabilities via single delegate, or via multiple delegates each implementing single capability.

Delegate' callbacks are invoked when you load Rune or call IRune::call().

struct AllInOneDelegate : public rune_vm::capabilities::IDelegate {
        AllInOneDelegate(const rune_vm::ILogger::CPtr& logger)
            : m_supportedCapabilities(g_supportedCapabilities.begin(), g_supportedCapabilities.end()) {}

        void setInput(const uint8_t* data, const uint32_t length) noexcept {
            m_input = rune_vm::DataView<const uint8_t>(data, length);
        }

    private:
        static constexpr auto g_supportedCapabilities = std::array{
            rune_vm::capabilities::Capability::Sound,
            rune_vm::capabilities::Capability::Accel,
            rune_vm::capabilities::Capability::Image,
            rune_vm::capabilities::Capability::Raw};

        // rune_vm::capabilities::IDelegate
        // return set of implemented capabilities to inform rune_vm runtime of what we are ready to work with
        [[nodiscard]] TCapabilitiesSet getSupportedCapabilities() const noexcept final {
            return m_supportedCapabilities;
        }

        // Requests specific capability from the user
        [[nodiscard]] bool requestCapability(
            const rune_vm::TRuneId runeId,
            const rune_vm::capabilities::Capability capability,
            const rune_vm::capabilities::TId newCapabilityId) noexcept final {
            // check if this delegates support request capability
            if(m_supportedCapabilities.count(capability) == 0)
                return false;

            // accept request for new capability allocation if everything is ok
            return true;
        }

        // Parametrizes capability id with some values. E.g. for image it might be its size
        [[nodiscard]] bool requestCapabilityParamChange(
            const rune_vm::TRuneId runeId,
            const rune_vm::capabilities::TId capabilityId,
            const rune_vm::capabilities::TKey& key,
            const rune_vm::capabilities::Parameter& parameter) noexcept final {
            // check the param applicability based on the capability id
            // use the param ...
            //
            // if everything is ok - accept param change request
            return true;
        }

        // This callback is called to request new input data for the Rune. Expect it to be invoked after you call IRune::call().
        [[nodiscard]] bool requestRuneInputFromCapability(
            const rune_vm::TRuneId runeId,
            const rune_vm::DataView<uint8_t> buffer,
            const rune_vm::capabilities::TId capabilityId) noexcept final {
            if(buffer.m_size != m_input->m_size) {
                // buffer size and your input size must match
                return false;
            }

            std::memcpy(buffer.m_data, m_input->m_data, buffer.m_size);
            m_input.reset();

            // we filled the buffer -> accept input request
            return true;
        }

        // data
        TCapabilitiesSet m_supportedCapabilities;
        std::optional<rune_vm::DataView<const uint8_t>> m_input;
    };

Create the context objects

First, you create the context - those objects are likely to be created only once per run.

// create rune_vm context
// logger is kept alive as shared_ptr inside rune_vm objects
auto logger = std::make_shared<StdoutLogger>();
// engine, runtime and rune objects must be kept alive by the user, so don't release shared_ptrs
auto engine = rune_vm::createEngine(
    logger,
    rune_vm::WasmBackend::Wasm3,
    std::max<rune_vm::TThreadCount>(1, std::thread::hardware_concurrency() - 1));
auto runtime = engine->createRuntime();

Do not change the constructor arguments unless you know what you are doing.

Load the Rune(-s)

Then, load one or multiple runes. The context is shared between runes, but the delegates shouldn't be shared between them. Note that you may pass multiple delegates to the IRuntime::loadRune function. This is because you can have e.g. single delegate per implemented capability. It's up to you, just make sure delegates' supported capability don't overlap between each other.

auto delegate = std::make_shared<AllInOneDelegate>(logger);
auto rune = runtime->loadRune({delegate}, pathToRuneFile);

Run the Rune(-s)

Now you have everything setup already. Now all you have to do is to update delegates' input and call IRune::call().

while(true) {
    // update the input
    delegate->setInput(data, length);
    // run the Rune
    auto result = rune->call();
    auto json = result->asJson();
    // parse the result
    // ...
}

Rune returns IResult::Ptr. Its composite object which contains a variable-length array with any of the supported (see IResult::Type) variable types as elements. For some cases it might be easier to just convert it to json via IResult::asJson() and parse it via any generic json library.