Skip to content

Commit c4ac1d3

Browse files
authored
[Add] Ability to set module resolve callback [Add] Import resolving tests (#21)
1 parent 3255318 commit c4ac1d3

File tree

3 files changed

+210
-18
lines changed

3 files changed

+210
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ cmake-build-*/
88
*.kdev4
99
*~
1010
CMakeSettings.json
11+
CMakeUserPresets.json
1112

1213
# doxygen
1314
docs/doxygen/*.md

include/wrenbind17/vm.hpp

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,38 @@ namespace std {
3333
* @ingroup wrenbind17
3434
*/
3535
namespace wrenbind17 {
36+
37+
namespace detail {
38+
inline std::string defaultLoadFileFn(const std::string& name) {
39+
40+
if (auto t = std::ifstream(name)) {
41+
std::string source((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
42+
return source;
43+
}
44+
45+
throw NotFound();
46+
}
47+
48+
inline void defaultPrintFn(const char* text) {
49+
std::cout << text;
50+
}
51+
52+
inline std::string defaultPathResolveFn(const std::vector<std::string>& paths, const std::string& importer,
53+
const std::string& name) {
54+
for (const auto& path : paths) {
55+
const auto test = path + "/" + std::string(name) + ".wren";
56+
57+
std::ifstream t(test);
58+
if (!t)
59+
continue;
60+
61+
return test;
62+
}
63+
64+
return name;
65+
}
66+
} // namespace detail
67+
3668
/**
3769
* @ingroup wrenbind17
3870
*/
@@ -41,7 +73,14 @@ namespace wrenbind17 {
4173
/**
4274
* @ingroup wrenbind17
4375
*/
44-
typedef std::function<std::string(const std::vector<std::string>& paths, const std::string& name)> LoadFileFn;
76+
typedef std::function<std::string(const std::string& name)> LoadFileFn;
77+
78+
/**
79+
* @ingroup wrenbind17
80+
*/
81+
typedef std::function<std::string(const std::vector<std::string>& paths, const std::string& importer,
82+
const std::string& name)>
83+
PathResolveFn;
4584

4685
/**
4786
* @ingroup wrenbind17
@@ -61,27 +100,18 @@ namespace wrenbind17 {
61100
: data(std::make_unique<Data>()) {
62101

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

69-
std::ifstream t(test);
70-
if (!t)
71-
continue;
72-
73-
std::string source((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
74-
return source;
75-
}
76-
77-
throw NotFound();
78-
};
104+
data->printFn = detail::defaultPrintFn;
105+
data->loadFileFn = detail::defaultLoadFileFn;
106+
data->pathResolveFn = detail::defaultPathResolveFn;
79107

80108
wrenInitConfiguration(&data->config);
109+
81110
data->config.initialHeapSize = initHeap;
82111
data->config.minHeapSize = minHeap;
83112
data->config.heapGrowthPercent = heapGrowth;
84113
data->config.userData = data.get();
114+
85115
#if WREN_VERSION_NUMBER >= 4000 // >= 0.4.0
86116
data->config.reallocateFn = [](void* memory, size_t newSize, void* userData) -> void* {
87117
return std::realloc(memory, newSize);
@@ -103,7 +133,7 @@ namespace wrenbind17 {
103133
}
104134

105135
try {
106-
auto source = self.loadFileFn(self.paths, std::string(name));
136+
auto source = self.loadFileFn(std::string(name));
107137
auto buffer = new char[source.size() + 1];
108138
std::memcpy(buffer, &source[0], source.size() + 1);
109139
res.source = buffer;
@@ -115,6 +145,14 @@ namespace wrenbind17 {
115145
}
116146
return res;
117147
};
148+
data->config.resolveModuleFn = [](WrenVM* vm, const char* importer, const char* name) -> const char* {
149+
auto& self = *reinterpret_cast<VM::Data*>(wrenGetUserData(vm));
150+
const auto resolved = self.pathResolveFn(self.paths, std::string(importer), std::string(name));
151+
auto buffer = new char[resolved.size() + 1];
152+
std::memcpy(buffer, &resolved[0], resolved.size() + 1);
153+
154+
return buffer;
155+
};
118156
#else // < 0.4.0
119157
data->config.reallocateFn = std::realloc;
120158
data->config.loadModuleFn = [](WrenVM* vm, const char* name) -> char* {
@@ -256,8 +294,9 @@ namespace wrenbind17 {
256294
* @throws CompileError if the compilation has failed
257295
*/
258296
inline void runFromModule(const std::string& name) {
259-
const auto source = data->loadFileFn(data->paths, name);
260-
runFromSource(name, source);
297+
const auto resolved = data->pathResolveFn(data->paths, "", name);
298+
const auto source = data->loadFileFn(resolved);
299+
runFromSource(resolved, source);
261300
}
262301

263302
/*!
@@ -338,6 +377,20 @@ namespace wrenbind17 {
338377
data->loadFileFn = fn;
339378
}
340379

380+
/*!
381+
* @brief Set a custom path resolver for imports
382+
* @see PathResolveFn
383+
* @details This must be a function that accepts a std::vector of strings
384+
* (which are the lookup paths from the constructor), the name of the importer as
385+
* the second parameter and the name of the import as the third.
386+
* Used for implementing relative paths. If the module is loaded through runFromModule
387+
* the importer parameter will be empty.
388+
* If you want to cancel the import, simply throw an exception.
389+
*/
390+
inline void setPathResolveFunc(const PathResolveFn& fn) {
391+
data->pathResolveFn = fn;
392+
}
393+
341394
/*!
342395
* @brief Runs the garbage collector
343396
*/
@@ -358,6 +411,7 @@ namespace wrenbind17 {
358411
std::string nextError;
359412
PrintFn printFn;
360413
LoadFileFn loadFileFn;
414+
PathResolveFn pathResolveFn;
361415

362416
inline void addClassType(const std::string& module, const std::string& name, const size_t hash) {
363417
classToModule.insert(std::make_pair(hash, module));

tests/path_resolve.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#include <catch2/catch.hpp>
2+
#include <filesystem>
3+
#include <wrenbind17/wrenbind17.hpp>
4+
5+
namespace wren = wrenbind17;
6+
using Filepath = std::filesystem::path;
7+
8+
std::string make_preferred(const std::string& path) {
9+
return Filepath(path).make_preferred().lexically_normal().string();
10+
}
11+
12+
TEST_CASE("ImportResolving") {
13+
14+
const std::string source1 = R"(
15+
import "game/classes/Player.wren" for Player
16+
)";
17+
18+
const std::string source2 = R"(
19+
import "classes/Player.wren" for Player
20+
)";
21+
22+
const std::string source3 = R"(
23+
import "classes/Player.wren" for Player
24+
import "game/classes/Player.wren" for Player
25+
import "game/../game/classes/Player.wren" for Player
26+
)";
27+
28+
const std::string file1 = R"(
29+
import "../game/classes/Player.wren" for Player
30+
)";
31+
32+
const std::string file2 = R"(
33+
import "Player.wren" for Player
34+
)";
35+
36+
const std::string file3 = R"(
37+
class Player {
38+
}
39+
)";
40+
41+
const std::pair<std::string, std::string> entry1 = {make_preferred("./game/MyGame.wren"), file1};
42+
const std::pair<std::string, std::string> entry2 = {make_preferred("./game/classes/Other.wren"), file2};
43+
const std::pair<std::string, std::string> entry3 = {make_preferred("./game/classes/Player.wren"), file3};
44+
45+
const std::unordered_map<std::string, std::string> filesystem{entry1, entry2, entry3};
46+
47+
wren::VM vm{{make_preferred("./"), make_preferred("./game")}};
48+
49+
vm.setPathResolveFunc([&filesystem](const std::vector<std::string>& paths, const std::string& importer,
50+
const std::string& name) -> std::string {
51+
auto parent = Filepath(importer).parent_path();
52+
auto relative = (Filepath(parent) / Filepath(name)).lexically_normal().make_preferred().string();
53+
54+
if (auto it = filesystem.find(relative); it != filesystem.end()) {
55+
return relative;
56+
}
57+
58+
for (const auto& path : paths) {
59+
60+
auto composed = (Filepath(path) / Filepath(name).lexically_normal().make_preferred()).string();
61+
if (auto it = filesystem.find(composed); it != filesystem.end()) {
62+
return composed;
63+
}
64+
}
65+
66+
return make_preferred(name);
67+
});
68+
69+
SECTION("Import from cwd") {
70+
71+
size_t modules_loaded = 0;
72+
73+
vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
74+
if (auto it = filesystem.find(path); it != filesystem.end()) {
75+
modules_loaded += 1;
76+
return it->second;
77+
}
78+
throw wren::NotFound();
79+
});
80+
81+
vm.runFromSource("User", source1);
82+
REQUIRE(modules_loaded == 1);
83+
}
84+
85+
SECTION("Import from include dir") {
86+
87+
size_t modules_loaded = 0;
88+
89+
vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
90+
if (auto it = filesystem.find(path); it != filesystem.end()) {
91+
modules_loaded += 1;
92+
return it->second;
93+
}
94+
throw wren::NotFound();
95+
});
96+
97+
vm.runFromSource("User", source2);
98+
REQUIRE(modules_loaded == 1);
99+
}
100+
101+
SECTION("Import from from script") {
102+
103+
size_t modules_loaded = 0;
104+
105+
vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
106+
if (auto it = filesystem.find(path); it != filesystem.end()) {
107+
modules_loaded += 1;
108+
return it->second;
109+
}
110+
throw wren::NotFound();
111+
});
112+
113+
vm.runFromModule("game/classes/Other.wren");
114+
vm.runFromModule("game/MyGame.wren");
115+
116+
REQUIRE(modules_loaded == 3);
117+
}
118+
119+
SECTION("Same Import Fail") {
120+
size_t modules_loaded = 0;
121+
122+
vm.setLoadFileFunc([&filesystem, &modules_loaded](const std::string& path) {
123+
if (auto it = filesystem.find(path); it != filesystem.end()) {
124+
modules_loaded += 1;
125+
return it->second;
126+
}
127+
throw wren::NotFound();
128+
});
129+
130+
try {
131+
vm.runFromSource("User", source3);
132+
REQUIRE(false); // This should fail since source3 imports the same script 3 times
133+
} catch (...) {
134+
REQUIRE(vm.getLastError() == "");
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)