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)