Bending bits...

Bytes and Words...

Safe call Guile functions from C++

While working on a C++ project in which I want to embedd Guile I needed to implement way to safe load and call Guile functions. Guile has a great and easy to use (in my opinion) C API and is easy to embedd it applications. Therefore, a perfect candidate for a scripting language to embedd in my C++ project. Therefore, I needed to implement a layer that loads Guile scripts and call functions, as safe as possible.

The project structure is as follows:

├── CMakeLists.txt
├── scripts
│   └── test.scm
└── src
    ├── CMakeLists.txt
    ├── guile_loader.hpp
    └── main.cpp

where in scripts/test.scm are the scripts to be loaded and executed and src/guile_loader.hpp is the header-only layer that loads Guile. I won’t go into details about the code, it’s use is pretty straightforward. The CMakeLists.txt files may be a little cumbersome, but I extracted them from some templates that I use.

Root CMake

# Minimum CMake version required
cmake_minimum_required(VERSION 3.20)

# Project name and version
project(cpp_guile VERSION 1.0)
set(PROJECT_NAME cpp_guile)

# Set the C standard to C99 (or C11 if preferred)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED True)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_library(project_options INTERFACE)

option(ENABLE_DEBUG "Enable Debug build" ON)

if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  # using Clang
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  target_compile_options(project_options INTERFACE -Wall -Wextra -O2)
  target_compile_options(project_options INTERFACE -Wall -Wextra -g)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel")
  # using Intel C++
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
  target_compile_options(project_options INTERFACE /W4 /Zi /utf-8)
  target_link_options(project_options INTERFACE /subsystem:console)
endif()



find_package(PkgConfig REQUIRED)
pkg_check_modules(GUILE REQUIRED guile-3.0)

option(ENABLE_PCH "Enable Precompiled Headers" ON)

if(ENABLE_PCH)
  target_precompile_headers(
    project_options
    INTERFACE
    <print>
    <memory>
    <exception>
    )
endif()



add_subdirectory("src")

src/CMakeLists.txt

include_directories(${GUILE_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/src/)

add_executable(${PROJECT_NAME} main.cpp)


target_link_libraries(
    ${PROJECT_NAME} PRIVATE
    project_options
    ${GUILE_LIBRARIES}
)

src/guile_loader.hpp

#pragma once
#include <libguile.h>

namespace safe::scm {
    class ScmException: public std::runtime_error {
    public:
        explicit ScmException (const std::string &msg): std::runtime_error(msg) {
        }

    };

    template <typename Func>
    auto call(Func func) -> SCM {
        auto func_ptr =
            std::make_shared<Func>(std::move(func));

        struct Context {
            std::shared_ptr<Func> func;
        };
        Context ctx{func_ptr};

        auto run = [](void *data) -> SCM {
            auto* ctx = static_cast<Context*>(data);
            return (*(ctx->func))();
        };

        auto error = []([[maybe_unused]] void *data, [[maybe_unused]] SCM key, SCM args) -> SCM {

            SCM msg_scm = ::scm_simple_format(
                SCM_BOOL_F,
                ::scm_from_utf8_string("Guile error: ~A"),
                ::scm_list_1(args)
                );


            char *msg = ::scm_to_locale_string(msg_scm);
            std::string errString(msg);
            free(msg);
            throw ScmException(errString);
        };

        return ::scm_c_catch(
            SCM_BOOL_T,
            run,
            &ctx,
            error,
            nullptr,
            nullptr,
            nullptr);
    }

};




class GuileLoader {
public:
    GuileLoader(const std::string& scriptPath) try:
        sScriptPath(scriptPath)
        {
            scm_init_guile();
            safe::scm::call([=] {
                return scm_c_primitive_load(scriptPath.c_str());
            });
        }
    catch (const safe::scm::ScmException& e){
        std::println("Failed to load Guile script: {}",e.what());
    }



    template<typename T>
    auto Execute(std::string_view)->std::optional<T>;


protected:
private:
    std::string sScriptPath;
};


template<typename T>
auto GuileLoader::Execute(std::string_view function_name) -> std::optional<T> {
    try {
        ::SCM script_function = safe::scm::call([&] {
            return scm_c_lookup(function_name.data());
        });
        ::SCM script_function_ref = safe::scm::call([&] {
            return scm_variable_ref(script_function);

        });

        ::SCM result = safe::scm::call([&] {
            return scm_call_0(script_function_ref);
        });
        if constexpr (std::is_same_v<T, int>) {
            return std::optional<int>(::scm_to_int(result));
        }
        if constexpr (std::is_same_v<T, double>) {
            return std::optional<double>(::scm_to_double(result));
        }
        if constexpr (std::is_same_v<T, float>) {
            return std::optional<float>(static_cast<float>(::scm_to_double(result)));
        }
        if constexpr (std::is_same_v<T, std::string>) {
            char *cstr = ::scm_to_utf8_string(result);
            std::string s(cstr);
            free(cstr);
            return std::optional<std::string>(s);
        }

    } catch(const safe::scm::ScmException& e) {
        std::println("Failed to execute Guile script: {}",e.what());
        return std::nullopt;
    }

}

src/main.cpp

#include "guile_loader.hpp"

auto main(int argc, char **argv) -> int
{

    GuileLoader loader{"../scripts/test.scm"};

    auto result = loader.Execute<int>("test");

    std::println("Result is {}", result.value());

    return 0;
}

scripts/test.scm

(define (test-function)
  (number->string (+ 2 3)))


(define (test)
  5)