diff --git a/include/functional.h b/include/functional.h new file mode 100644 index 00000000..afd3ef74 --- /dev/null +++ b/include/functional.h @@ -0,0 +1,112 @@ +#pragma once + +#include "traits.h" + +namespace ink::runtime::internal +{ + class basic_eval_stack; + + // base function container with virtual callback methods + class function_base + { + public: + virtual ~function_base() { } + + // calls the underlying function object taking parameters from a stack + virtual void call(basic_eval_stack* stack, size_t length) = 0; + + protected: + // used to hide basic_eval_stack and value definitions + template + static T pop(basic_eval_stack* stack); + + // used to hide basic_eval_stack and value definitions + template + static void push(basic_eval_stack* stack, const T& value); + }; + + // Stores a Callable function object and forwards calls to it + template + class function : public function_base + { + public: + function(F functor) : functor(functor) { } + + // calls the underlying function using arguments on the stack + virtual void call(basic_eval_stack* stack, size_t length) override + { + call(stack, length, GenSeq()); + } + + private: + // Callable functor object + F functor; + + // function traits + using traits = function_traits; + + // argument types + template + using arg_type = typename function_traits::argument::type; + + // pops an argument from the stack using the function-type + template + typename arg_type pop_arg(basic_eval_stack* stack) + { + // todo - type assert? + + return pop>(stack); + } + + template + void call(basic_eval_stack* stack, size_t length, seq) + { + // Make sure the argument counts match + inkAssert(sizeof...(Is) == length, "Attempting to call functor with too few/many arguments"); + static_assert(sizeof...(Is) == traits::arity); + + // void functions + if constexpr (is_same::value) + { + // Just evalulate + functor(pop_arg(stack)...); + + // Ink expects us to push something + // TODO -- Should be a special "void" value + push(stack, 0); + } + else + { + // Evaluate and push the result onto the stack + push(stack, functor(pop_arg(stack)...)); + } + } + }; + +#ifdef INK_ENABLE_UNREAL + template + class function_array_delegate : public function_base + { + public: + function_array_delegate(const D& del) : invocableDelegate(del) { } + + // calls the underlying delegate using arguments on the stack + virtual void call(basic_eval_stack* stack, size_t length) override + { + // Create variable array + TArray variables; + for (size_t i = 0; i < length; i++) + { + variables.Add(pop(stack)); + } + + FInkVar result; + invocableDelegate.ExecuteIfBound(variables, result); + + push(stack, result); + } + private: + D invocableDelegate; + }; +#endif +} \ No newline at end of file diff --git a/include/runner.h b/include/runner.h index 233aa8fa..28604341 100644 --- a/include/runner.h +++ b/include/runner.h @@ -2,6 +2,7 @@ #include "config.h" #include "system.h" +#include "functional.h" #ifdef INK_ENABLE_UNREAL #include "Containers/UnrealString.h" @@ -127,6 +128,34 @@ namespace ink::runtime * @param index index of the choice to make */ virtual void choose(size_t index) = 0; + + protected: + // internal bind implementation. not for calling. + virtual void internal_bind(hash_t name, internal::function_base* function) = 0; + public: + /** + * Binds an external callable to the runtime + * + * Given a name and a callable object, register this function + * to be called back from the ink runtime. + * + * @param name name hash + * @param function callable + */ + template + void bind(hash_t name, F function) + { + internal_bind(name, new internal::function(function)); + } + +#ifdef INK_ENABLE_UNREAL + template + void bind_delegate(hash_t name, D functionDelegate) + { + internal_bind(name, new internal::function_array_delegate(functionDelegate)); + } +#endif + #pragma endregion #pragma region Convenience Methods diff --git a/include/story_ptr.h b/include/story_ptr.h index f52e1bc5..caba3eac 100644 --- a/include/story_ptr.h +++ b/include/story_ptr.h @@ -90,7 +90,14 @@ namespace ink::runtime : story_ptr_base(nullptr, nullptr) , _ptr(nullptr) { - assert(ptr == nullptr, "can not create story_ptr from existing pointer!"); + inkAssert(ptr == nullptr, "can not create story_ptr from existing pointer!"); + } + + // null constructor + story_ptr() + : story_ptr_base(nullptr, nullptr) + , _ptr(nullptr) + { } // destructor @@ -105,7 +112,12 @@ namespace ink::runtime story_ptr cast() { // if cast fails, return null +#ifdef INK_ENABLE_UNREAL + // Unreal disables RTTI + U* casted = reinterpret_cast(_ptr); +#else U* casted = dynamic_cast(_ptr); +#endif if (casted == nullptr) return nullptr; diff --git a/include/traits.h b/include/traits.h new file mode 100644 index 00000000..f94ca952 --- /dev/null +++ b/include/traits.h @@ -0,0 +1,116 @@ +#pragma once + +namespace ink::runtime::internal +{ + template + struct get + { + using type = typename get::type; + }; + + template + struct get<0, Arg, Args...> + { + using type = typename Arg; + }; + + // constant and is_same from http://www.cppreference.com + + template + struct constant { + static constexpr T value = v; + typedef T value_type; + typedef constant type; // using injected-class-name + constexpr operator value_type() const noexcept { return value; } + constexpr value_type operator()() const noexcept { return value; } //since c++14 + }; + + template + struct is_same : constant {}; + + template + struct is_same : constant {}; + + // function_traits from https://functionalcpp.wordpress.com/2013/08/05/function-traits/ + + template + struct function_traits; + + // function pointer + template + struct function_traits : public function_traits + {}; + + template + struct function_traits + { + using return_type = R; + + static constexpr unsigned int arity = sizeof...(Args); + + template + struct argument + { + static_assert(N < arity, "error: invalid parameter index."); + using type = typename get::type; + }; + }; + + // member function pointer + template + struct function_traits : public function_traits + {}; + + // const member function pointer + template + struct function_traits : public function_traits + {}; + + // member object pointer + template + struct function_traits : public function_traits + {}; + + // functor + template + struct function_traits + { + private: + using call_type = function_traits; + public: + using return_type = typename call_type::return_type; + + static constexpr unsigned int arity = call_type::arity - 1; + + template + struct argument + { + static_assert(N < arity, "error: invalid parameter index."); + using type = typename call_type::template argument::type; + }; + }; + + // from https://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence + // using aliases for cleaner syntax + template using Invoke = typename T::type; + + template struct seq { using type = seq; }; + + template struct concat; + + template + struct concat, seq> + : seq {}; + + template + using Concat = Invoke>; + + template struct gen_seq; + template using GenSeq = Invoke>; + + template + struct gen_seq : Concat, GenSeq> {}; + + template<> struct gen_seq<0> : seq<> {}; + template<> struct gen_seq<1> : seq<0> {}; +} \ No newline at end of file diff --git a/inkcpp/command.h b/inkcpp/command.h index 248fa775..de88630e 100644 --- a/inkcpp/command.h +++ b/inkcpp/command.h @@ -60,14 +60,19 @@ namespace ink MAX, BINARY_OPERATORS_END = MAX, + // == Unary operators UNARY_OPERATORS_START, NOT = UNARY_OPERATORS_START, NEGATE, UNARY_OPERATORS_END = NEGATE, + // == Container tracking START_CONTAINER_MARKER, END_CONTAINER_MARKER, + // == Function calls + CALL_EXTERNAL, + NUM_COMMANDS, }; @@ -151,6 +156,8 @@ namespace ink "!", "~", + nullptr, + nullptr, nullptr }; diff --git a/inkcpp/compiler.cpp b/inkcpp/compiler.cpp index 115adf13..11db08c9 100644 --- a/inkcpp/compiler.cpp +++ b/inkcpp/compiler.cpp @@ -307,7 +307,7 @@ namespace ink { write_path(data, Command::DIVERT, path, self, flag); } } - else if (iter->find("^->") != iter->end()) + else if (iter->find("^->") != iter->end()) // divert to value { // Get the divert path auto path = (*iter)["^->"].get(); @@ -352,6 +352,19 @@ namespace ink { // Write out path. Speciically, we want the post-processor to write out the counter index for this container write_path(data, Command::READ_COUNT, path, self, CommandFlag::NO_FLAGS, true); } + // external function call + else if (iter->find("x()") != iter->end()) + { + // Get name and argument count + auto name = (*iter)["x()"].get(); + int numArgs = + iter->find("exArgs") == iter->end() + ? 0 + : (*iter)["exArgs"].get(); + + // Encode num arguments into command flag and write out the hash of the function name as the parameter + write(data, Command::CALL_EXTERNAL, hash_string(name.c_str()), (CommandFlag)numArgs); + } } } diff --git a/inkcpp/functional.cpp b/inkcpp/functional.cpp new file mode 100644 index 00000000..d4d28784 --- /dev/null +++ b/inkcpp/functional.cpp @@ -0,0 +1,85 @@ +#include "functional.h" + +#include "value.h" +#include "stack.h" + +#ifdef INK_ENABLE_UNREAL +#include "InkVar.h" +#endif + +namespace ink::runtime::internal +{ + template + static T function_base::pop(basic_eval_stack* stack) + { + return stack->pop().get(); + } + + template + static void function_base::push(basic_eval_stack* stack, const T& value) + { + stack->push(value); + } + + // Generate template implementations for all significant types + +#define SUPPORT_TYPE(TYPE) template TYPE function_base::pop(basic_eval_stack*); template void function_base::push(basic_eval_stack*, const TYPE&) +#define SUPPORT_TYPE_PARAMETER_ONLY(TYPE) template TYPE function_base::pop(basic_eval_stack*) + + SUPPORT_TYPE(int); + SUPPORT_TYPE(float); + SUPPORT_TYPE(uint32_t); + + // TODO - Support string return values + +#ifdef INK_ENABLE_STL + SUPPORT_TYPE_PARAMETER_ONLY(std::string); +#endif +#ifdef INK_ENABLE_UNREAL + SUPPORT_TYPE_PARAMETER_ONLY(FString); + + template<> + FInkVar function_base::pop(basic_eval_stack* stack) + { + value v = stack->pop(); + switch (v.type()) + { + case value_type::null: + case value_type::divert: + inkFail("Trying to pass null or divert as ink parameter to external function"); + break; + case value_type::integer: + return FInkVar(v.get()); + case value_type::decimal: + return FInkVar(v.get()); + case value_type::string: + return FInkVar(v.get()); + } + + return FInkVar(); + } + + template<> + void function_base::push(basic_eval_stack* stack, const FInkVar& value) + { + switch (value.type) + { + case EInkVarType::None: + { + internal::value v; + stack->push(v); + } + break; + case EInkVarType::Int: + stack->push(value.intVar); + break; + case EInkVarType::Float: + stack->push(value.floatVar); + break; + case EInkVarType::String: + inkFail("NOT IMPLEMENTED"); // TODO: String support + return; + } + } +#endif +} \ No newline at end of file diff --git a/inkcpp/functions.cpp b/inkcpp/functions.cpp new file mode 100644 index 00000000..631f3674 --- /dev/null +++ b/inkcpp/functions.cpp @@ -0,0 +1,57 @@ +#include "functions.h" + +namespace ink::runtime::internal +{ + functions::functions() : _list(nullptr), _last(nullptr) + { + } + + functions::~functions() + { + // clean list + while (_list) + { + entry* toDelete = _list; + _list = _list->next; + + // delete both value and entry + delete toDelete->value; + delete toDelete; + } + _list = _last = nullptr; + } + + void functions::add(hash_t name, function_base* func) + { + entry* current = new entry; + current->name = name; + current->value = func; + current->next = nullptr; + + if (_list == nullptr) + { + _list = _last = current; + } + else + { + _last->next = current; + _last = current; + } + } + + bool functions::call(hash_t name, basic_eval_stack* stack, size_t num_arguments) + { + // find entry + entry* iter = _list; + while (iter != nullptr && iter->name != name) + iter = iter->next; + + // failed to find + if (iter == nullptr) + return false; + + // call + iter->value->call(stack, num_arguments); + return true; + } +} \ No newline at end of file diff --git a/inkcpp/functions.h b/inkcpp/functions.h new file mode 100644 index 00000000..55e65885 --- /dev/null +++ b/inkcpp/functions.h @@ -0,0 +1,35 @@ +#pragma once + +#include "functional.h" +#include "system.h" + +namespace ink::runtime::internal +{ + class basic_eval_stack; + + // Stores bound functions + class functions + { + public: + functions(); + ~functions(); + + // Adds a function to the registry + void add(hash_t name, function_base* func); + + // Calls a function (if available) + bool call(hash_t name, basic_eval_stack* stack, size_t num_arguments); + + private: + struct entry + { + hash_t name; + function_base* value; + entry* next; + }; + + // TODO: Better than a linked list? + entry* _list; + entry* _last; + }; +} \ No newline at end of file diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 38bbab24..70a2b08e 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -322,6 +322,13 @@ namespace ink::runtime::internal reset(); _ptr = _story->instructions(); jump(destination); + + return true; + } + + void runner_impl::internal_bind(hash_t name, internal::function_base* function) + { + _functions.add(name, function); } runner_impl::change_type runner_impl::detect_change() const @@ -532,6 +539,22 @@ namespace ink::runtime::internal } break; + // == Function calls + case Command::CALL_EXTERNAL: + { + // Read function name + hash_t functionName = read(); + + // Interpret flag as argument count + int numArguments = (int)flag; + + // find and execute. will automatically push a valid if applicable + _functions.call(functionName, &_eval, numArguments); + + // TODO: Verify something was found? + } + break; + // == Evaluation stack case Command::START_EVAL: bEvaluationMode = true; diff --git a/inkcpp/runner_impl.h b/inkcpp/runner_impl.h index 85084bd8..2e440e23 100644 --- a/inkcpp/runner_impl.h +++ b/inkcpp/runner_impl.h @@ -8,6 +8,7 @@ #include "config.h" #include "simple_restorable_stack.h" #include "types.h" +#include "functions.h" #include "runner.h" #include "choice.h" @@ -56,6 +57,9 @@ namespace ink::runtime::internal virtual FString getline() override; #endif #pragma endregion + protected: + // bind external + virtual void internal_bind(hash_t name, internal::function_base* function) override; private: // Advances the interpreter by a line. This fills the output buffer void advance_line(); @@ -126,11 +130,14 @@ namespace ink::runtime::internal choice _choices[MAX_CHOICES]; size_t _num_choices = 0; + // TODO: Move to story? Both? + functions _functions; + // Container set internal::restorable_stack _container; bool _is_falling = false; - bool _saved; + bool _saved = false; }; #ifdef INK_ENABLE_STL diff --git a/inkcpp/system.cpp b/inkcpp/system.cpp index 56df80fa..2136f825 100644 --- a/inkcpp/system.cpp +++ b/inkcpp/system.cpp @@ -1,5 +1,7 @@ #include "system.h" +#ifndef INK_ENABLE_UNREAL + namespace ink { #define A 54059 /* a prime */ @@ -30,4 +32,6 @@ namespace ink throw ink_exception(msg); } -} \ No newline at end of file +} + +#endif \ No newline at end of file diff --git a/inkcpp/value.h b/inkcpp/value.h index 3688dbc6..cf351ff9 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -2,6 +2,10 @@ #include "system.h" +#ifdef INK_ENABLE_STL +#include +#endif + namespace ink { namespace runtime @@ -83,6 +87,27 @@ namespace ink uint32_t as_divert() const { return _first.uint_value; } // TODO: String access? + template + T get() const { static_assert(false); } + + // == TODO: Asserts? + + template<> + int get() const { return as_int(); } + template<> + float get() const { return as_float(); } + template<> + uint32_t get() const { return as_divert(); } + +#ifdef INK_ENABLE_STL + template<> + std::string get() const { return _first.string_val; } // TODO: Missing amalgamate? +#endif +#ifdef INK_ENABLE_UNREAL + template<> + FString get() const { return _first.string_val; } // TODO: Missing amalgamate? +#endif + inline operator int() const { return as_int(); } inline operator float() const { return as_float(); } inline operator uint32_t() const { return as_divert(); } diff --git a/inkcpp_cl/inkcpp_cl.cpp b/inkcpp_cl/inkcpp_cl.cpp index 6db0ca9c..b98601d2 100644 --- a/inkcpp_cl/inkcpp_cl.cpp +++ b/inkcpp_cl/inkcpp_cl.cpp @@ -10,6 +10,11 @@ #include #include +int x(int a, int b) +{ + return a + b; +} + int main() { { @@ -27,6 +32,9 @@ int main() // Start runner runner thread = myInk->new_runner(); + // Register external functions + thread->bind(ink::hash_string("brook"), &x); + // move to knot thread->move_to(ink::hash_string("knot_1")); diff --git a/inkcpp_cl/test.ink b/inkcpp_cl/test.ink index bcc98147..4bfb166c 100644 --- a/inkcpp_cl/test.ink +++ b/inkcpp_cl/test.ink @@ -1,9 +1,7 @@ -- (start) -Look at this crazy list: {&A|B|C|D}! -Continue? -+ Continue --> start - -=== knot_1 -This is a knot! +EXTERNAL brook(x, y) + +My ink file {brook(4, 2)}. + + + ->END \ No newline at end of file diff --git a/inkcpp_cl/test.json b/inkcpp_cl/test.json index 7628bc3e..83599d94 100644 --- a/inkcpp_cl/test.json +++ b/inkcpp_cl/test.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["^Look at this crazy list: ",["ev","visit",4,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^A",{"->":".^.^.29"},null],"s1":["pop","^B",{"->":".^.^.29"},null],"s2":["pop","^C",{"->":".^.^.29"},null],"s3":["pop","^D",{"->":".^.^.29"},null],"#f":5}],"^!","\n","^Continue?","\n",["ev",{"^->":"0.start.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Continue",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.start.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.6.s"},[{"#n":"$r2"}],"\n",{"->":".^.^"},{"->":"0.g-0"},{"#f":5}],"#f":5,"#n":"start"}],{"g-0":["done",{"#f":5}]}],"done",{"knot_1":["^This is a knot!","\n","end",{"#f":1}],"#f":1}],"listDefs":{}} \ No newline at end of file +{"inkVersion":19,"root":[["^My ink file ","ev",4,2,{"x()":"brook","exArgs":2},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file