Skip to content

Commit

Permalink
[Add] Ability to set module resolve callback [Add] Import resolving t…
Browse files Browse the repository at this point in the history
…ests
  • Loading branch information
santiberna committed Nov 29, 2024
1 parent 3255318 commit 65d7320
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cmake-build-*/
*.kdev4
*~
CMakeSettings.json
CMakeUserPresets.json

# doxygen
docs/doxygen/*.md
Expand Down
90 changes: 72 additions & 18 deletions include/wrenbind17/vm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ namespace std {
* @ingroup wrenbind17
*/
namespace wrenbind17 {

namespace detail {
inline std::string defaultLoadFileFn(const std::string& name) {

if (auto t = std::ifstream(name)) {
std::string source((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
return source;
}

throw NotFound();
}

inline void defaultPrintFn(const char* text) {
std::cout << text;
}

inline std::string defaultPathResolveFn(const std::vector<std::string>& paths, const std::string& importer,
const std::string& name) {
for (const auto& path : paths) {
const auto test = path + "/" + std::string(name) + ".wren";

std::ifstream t(test);
if (!t)
continue;

return test;
}

return name;
}
} // namespace detail

/**
* @ingroup wrenbind17
*/
Expand All @@ -41,7 +73,14 @@ namespace wrenbind17 {
/**
* @ingroup wrenbind17
*/
typedef std::function<std::string(const std::vector<std::string>& paths, const std::string& name)> LoadFileFn;
typedef std::function<std::string(const std::string& name)> LoadFileFn;

/**
* @ingroup wrenbind17
*/
typedef std::function<std::string(const std::vector<std::string>& paths, const std::string& importer,
const std::string& name)>
PathResolveFn;

/**
* @ingroup wrenbind17
Expand All @@ -61,27 +100,18 @@ namespace wrenbind17 {
: data(std::make_unique<Data>()) {

data->paths = std::move(paths);
data->printFn = [](const char* text) -> void { std::cout << text; };
data->loadFileFn = [](const std::vector<std::string>& paths, const std::string& name) -> std::string {
for (const auto& path : paths) {
const auto test = path + "/" + std::string(name) + ".wren";

std::ifstream t(test);
if (!t)
continue;

std::string source((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
return source;
}

throw NotFound();
};
data->printFn = detail::defaultPrintFn;
data->loadFileFn = detail::defaultLoadFileFn;
data->pathResolveFn = detail::defaultPathResolveFn;

wrenInitConfiguration(&data->config);

data->config.initialHeapSize = initHeap;
data->config.minHeapSize = minHeap;
data->config.heapGrowthPercent = heapGrowth;
data->config.userData = data.get();

#if WREN_VERSION_NUMBER >= 4000 // >= 0.4.0
data->config.reallocateFn = [](void* memory, size_t newSize, void* userData) -> void* {
return std::realloc(memory, newSize);
Expand All @@ -103,7 +133,7 @@ namespace wrenbind17 {
}

try {
auto source = self.loadFileFn(self.paths, std::string(name));
auto source = self.loadFileFn(std::string(name));
auto buffer = new char[source.size() + 1];
std::memcpy(buffer, &source[0], source.size() + 1);
res.source = buffer;
Expand All @@ -115,6 +145,14 @@ namespace wrenbind17 {
}
return res;
};
data->config.resolveModuleFn = [](WrenVM* vm, const char* importer, const char* name) -> const char* {
auto& self = *reinterpret_cast<VM::Data*>(wrenGetUserData(vm));
const auto resolved = self.pathResolveFn(self.paths, std::string(importer), std::string(name));
auto buffer = new char[resolved.size() + 1];
std::memcpy(buffer, &resolved[0], resolved.size() + 1);

return buffer;
};
#else // < 0.4.0
data->config.reallocateFn = std::realloc;
data->config.loadModuleFn = [](WrenVM* vm, const char* name) -> char* {
Expand Down Expand Up @@ -256,8 +294,9 @@ namespace wrenbind17 {
* @throws CompileError if the compilation has failed
*/
inline void runFromModule(const std::string& name) {
const auto source = data->loadFileFn(data->paths, name);
runFromSource(name, source);
const auto resolved = data->pathResolveFn(data->paths, "", name);
const auto source = data->loadFileFn(resolved);
runFromSource(resolved, source);
}

/*!
Expand Down Expand Up @@ -338,6 +377,20 @@ namespace wrenbind17 {
data->loadFileFn = fn;
}

/*!
* @brief Set a custom path resolver for imports
* @see PathResolveFn
* @details This must be a function that accepts a std::vector of strings
* (which are the lookup paths from the constructor), the name of the importer as
* the second parameter and the name of the import as the third.
* Used for implementing relative paths. If the module is loaded through runFromModule
* the importer parameter will be empty.
* If you want to cancel the import, simply throw an exception.
*/
inline void setPathResolveFunc(const PathResolveFn& fn) {
data->pathResolveFn = fn;
}

/*!
* @brief Runs the garbage collector
*/
Expand All @@ -358,6 +411,7 @@ namespace wrenbind17 {
std::string nextError;
PrintFn printFn;
LoadFileFn loadFileFn;
PathResolveFn pathResolveFn;

inline void addClassType(const std::string& module, const std::string& name, const size_t hash) {
classToModule.insert(std::make_pair(hash, module));
Expand Down
137 changes: 137 additions & 0 deletions tests/path_resolve.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include <catch2/catch.hpp>
#include <filesystem>
#include <wrenbind17/wrenbind17.hpp>

namespace wren = wrenbind17;
using Filepath = std::filesystem::path;

std::string make_preferred(const std::string& path) {
return Filepath(path).make_preferred().lexically_normal().string();
}

TEST_CASE("ImportResolving") {

const std::string source1 = R"(
import "game/classes/Player.wren" for Player
)";

const std::string source2 = R"(
import "classes/Player.wren" for Player
)";

const std::string source3 = R"(
import "classes/Player.wren" for Player
import "game/classes/Player.wren" for Player
import "game/../game/classes/Player.wren" for Player
)";

const std::string file1 = R"(
import "../game/classes/Player.wren" for Player
)";

const std::string file2 = R"(
import "Player.wren" for Player
)";

const std::string file3 = R"(
class Player {
}
)";

const std::pair<std::string, std::string> entry1 = {make_preferred("./game/MyGame.wren"), file1};
const std::pair<std::string, std::string> entry2 = {make_preferred("./game/classes/Other.wren"), file2};
const std::pair<std::string, std::string> entry3 = {make_preferred("./game/classes/Player.wren"), file3};

const std::unordered_map<std::string, std::string> filesystem{entry1, entry2, entry3};

wren::VM vm{{make_preferred("./"), make_preferred("./game")}};

vm.setPathResolveFunc([&filesystem](const std::vector<std::string>& paths, const std::string& importer,
const std::string& name) -> std::string {
auto parent = Filepath(importer).parent_path();
auto relative = (Filepath(parent) / Filepath(name)).lexically_normal().make_preferred().string();

if (auto it = filesystem.find(relative); it != filesystem.end()) {
return relative;
}

for (const auto& path : paths) {

auto composed = (Filepath(path) / Filepath(name).lexically_normal().make_preferred()).string();
if (auto it = filesystem.find(composed); it != filesystem.end()) {
return composed;
}
}

return make_preferred(name);
});

SECTION("Import from cwd") {

size_t modules_loaded = 0;

vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
if (auto it = filesystem.find(path); it != filesystem.end()) {
modules_loaded += 1;
return it->second;
}
throw wren::NotFound();
});

vm.runFromSource("User", source1);
REQUIRE(modules_loaded == 1);
}

SECTION("Import from include dir") {

size_t modules_loaded = 0;

vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
if (auto it = filesystem.find(path); it != filesystem.end()) {
modules_loaded += 1;
return it->second;
}
throw wren::NotFound();
});

vm.runFromSource("User", source2);
REQUIRE(modules_loaded == 1);
}

SECTION("Import from from script") {

size_t modules_loaded = 0;

vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
if (auto it = filesystem.find(path); it != filesystem.end()) {
modules_loaded += 1;
return it->second;
}
throw wren::NotFound();
});

vm.runFromModule("game/classes/Other.wren");
vm.runFromModule("game/MyGame.wren");

REQUIRE(modules_loaded == 3);
}

SECTION("Same Import Fail") {
size_t modules_loaded = 0;

vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
if (auto it = filesystem.find(path); it != filesystem.end()) {
modules_loaded += 1;
return it->second;
}
throw wren::NotFound();
});

try {
vm.runFromSource("User", source3);
REQUIRE(false); // This should fail since source3 imports the same script 3 times
} catch (...) {
REQUIRE(vm.getLastError() == "");
}
}
}

0 comments on commit 65d7320

Please sign in to comment.