From 947d6ff736dfc3ad44083d0075b8c28dc9079aee Mon Sep 17 00:00:00 2001 From: clem Date: Wed, 23 Feb 2022 21:08:42 +0100 Subject: [PATCH 01/56] stashing --- .test/main.cpp | 4 +++ CMakeLists.txt | 2 +- include/inline_eval.hpp | 14 ++++++++++ include/jluna.jl | 62 +++++++++++++++++++++++++++++++++++++++++ include/usertype.hpp | 23 +++++++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 include/inline_eval.hpp create mode 100644 include/usertype.hpp diff --git a/.test/main.cpp b/.test/main.cpp index 313a854..62ee2fd 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -9,9 +9,13 @@ using namespace jluna; using namespace jluna::detail; + int main() { State::initialize(); + + auto a = "test"_a; + return 0; Test::initialize(); Test::test("catch c exception", [](){ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cd4a42..974088d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,7 @@ add_library(jluna SHARED include/generator_expression.hpp .src/generator_expression.cpp -) + include/usertype.hpp include/inline_eval.hpp) set_target_properties(jluna PROPERTIES LINKER_LANGUAGE C diff --git a/include/inline_eval.hpp b/include/inline_eval.hpp new file mode 100644 index 0000000..834630d --- /dev/null +++ b/include/inline_eval.hpp @@ -0,0 +1,14 @@ +// +// Copyright 2022 Clemens Cords +// Created on 23.02.22 by clem (mail@clemens-cords.com) +// + +#pragma once + +#include + +namespace jluna +{ + //Any* operator""_eval(const char* cs, size_t); + +} \ No newline at end of file diff --git a/include/jluna.jl b/include/jluna.jl index 52cddcd..f2f5e99 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -951,6 +951,68 @@ module jluna return false end end + + module usertype + + """ + `new_struct(::Symbol) -> Expr` + """ + function new_struct(name::Symbol) ::Expr + return Expr(:struct, false, name, Expr(:block)) + end + + """ + `set_mutable!(::Expr, ::Bool = true) -> Nothing` + + modify struct expressions mutability + """ + function set_mutable!(expr::Expr, is_mutable::Bool = true) ::Nothing + + expr.args[1] = is_mutable + return nothing + end + + """ + `add_field!(::Expr, ::Symbol, ::Type = Any) -> Nothing` + + add explicitly types field to struct expression + """ + function add_field!(expr::Expr, field_name::Symbol, type::Type = Any) ::Nothing + + push!(expr.args[3].args, Expr(:(::), field_name, Symbol(type))) + return nothing + end + + # overload to be used with symbols such as parameter names + function add_field!(expr::Expr, field_name::Symbol, type::Symbol) ::Nothing + + push!(expr.args[3].args, Expr(:(::), field_name, type)) + return nothing + end + + """ + `add_parameter(::Expr, ::Symbol, ::Type = Any) -> Nothing` + + add parameters to struct expression + """ + function add_parameter!(expr::Expr, param_name::Symbol, type::Type = Any) ::Nothing + + if !(expr.args[2] isa Expr) + expr.args[2] = Expr(:curly, expr.args[2], Expr(:(<:), param_name, Symbol(Type))) + else + push!(expr.args[2].args, Expr(:(<:), param_name, Symbol(Type))) + end + + return nothing; + end + + """ + """ + function add_constructor!(expr::Expr, f::Function) ::Nothing + push!(expr.args[3].args, Expr(:(=), Expr(:call, :Test, ))) + end + + end end """ diff --git a/include/usertype.hpp b/include/usertype.hpp new file mode 100644 index 0000000..aba8deb --- /dev/null +++ b/include/usertype.hpp @@ -0,0 +1,23 @@ +// +// Copyright 2022 Clemens Cords +// Created on 22.02.22 by clem (mail@clemens-cords.com) +// + +#pragma once + +#include + +namespace jluna +{ + auto* a = (jl_datatype_t*) jl_array_type; + + + class Usertype + { + public: + + + private: + Any* _expression; + }; +} \ No newline at end of file From 3bdcc56d8695587c306d12b284bd04d090aef7d4 Mon Sep 17 00:00:00 2001 From: clem Date: Thu, 24 Feb 2022 22:28:57 +0100 Subject: [PATCH 02/56] stashing before rework --- .src/exceptions.cpp | 23 +++++++++- .test/main.cpp | 11 +++-- include/exceptions.hpp | 9 ++++ include/inline_eval.hpp | 14 ------ include/jluna.jl | 89 +++++++++++++++++++++++++++++-------- include/julia_extension.hpp | 8 ++++ include/usertype.hpp | 1 + 7 files changed, 118 insertions(+), 37 deletions(-) delete mode 100644 include/inline_eval.hpp diff --git a/.src/exceptions.cpp b/.src/exceptions.cpp index 79678d0..e0c3f85 100644 --- a/.src/exceptions.cpp +++ b/.src/exceptions.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace jluna { @@ -55,9 +56,29 @@ namespace jluna jl_eval_string("return jluna.exception_handler.get_last_exception()"), jl_to_string(jl_eval_string("return jluna.exception_handler.get_last_message()")) ); - return; } + } + + Any* safe_eval(const char* str) + { + jluna::throw_if_uninitialized(); + static jl_function_t* safe_call = jl_find_function("jluna.exception_handler", "safe_call"); + + jl_gc_pause; + auto* result = jl_call1(safe_call, jl_quote(str)); + if (jl_exception_occurred() or jl_unbox_bool(jl_eval_string("jluna.exception_handler.has_exception_occurred()"))) + { + std::cerr << "exception in jluna::State::safe_eval for expression:\n\"" << str << "\"\n" << std::endl; + forward_last_exception(); + } + jl_gc_unpause; + return result; + } + + Any* operator""_eval(const char* str, size_t _) + { + return safe_eval(str); } } diff --git a/.test/main.cpp b/.test/main.cpp index 62ee2fd..935f072 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -14,8 +14,8 @@ int main() { State::initialize(); - auto a = "test"_a; - return 0; + jluna::safe_eval("throw(ErrorException(\"abc\"))"); +return 0; Test::initialize(); Test::test("catch c exception", [](){ @@ -26,7 +26,6 @@ int main() }); }); - Test::test("jl_find_function", [](){ auto* expected = jl_get_function(jl_base_module, "println"); @@ -47,6 +46,12 @@ int main() }); }); + Test::test("safe_eval", [](){ + Test::assert_that_throws([]() { + safe_eval("throw(ErrorException(\"abc\"))"); + }); + }); + auto test_box_unbox = [](const std::string type_name, T value) { Test::test("box/unbox " + type_name , [value]() { diff --git a/include/exceptions.hpp b/include/exceptions.hpp index 4d47333..610197a 100644 --- a/include/exceptions.hpp +++ b/include/exceptions.hpp @@ -71,6 +71,15 @@ namespace jluna /// @returns result template Any* safe_call(Function* function, Args_t... args); + + /// @brief evaluate string with exception forwarding + /// @param string + /// @returns result + Any* safe_eval(const char*); + + /// @brief literal operator for prettier syntax + /// @returns result of safe_eval + Any* operator""_eval(const char*, size_t); } #include ".src/exceptions.inl" \ No newline at end of file diff --git a/include/inline_eval.hpp b/include/inline_eval.hpp deleted file mode 100644 index 834630d..0000000 --- a/include/inline_eval.hpp +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright 2022 Clemens Cords -// Created on 23.02.22 by clem (mail@clemens-cords.com) -// - -#pragma once - -#include - -namespace jluna -{ - //Any* operator""_eval(const char* cs, size_t); - -} \ No newline at end of file diff --git a/include/jluna.jl b/include/jluna.jl index f2f5e99..5e17f3c 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -954,64 +954,106 @@ module jluna module usertype + # template that can still be modified, transformed into actual type with `implement` + struct UserTypeTemplate + + _expr::Expr + _member_functions::Dict{Symbol, Union{Function, jluna._cppcall.UnnamedFunctionProxy}} + + UserTypeTemplate(expr::Expr) = new(expr, Vector{Function}()) + end + + + mutable struct UserType + + _name::Symbol + _members::Dict{Symbol, Any} + end + + struct ImmutableUserType <: UserType + end + + mutable struct MutableUserType <: UserType + end + + """ `new_struct(::Symbol) -> Expr` """ - function new_struct(name::Symbol) ::Expr - return Expr(:struct, false, name, Expr(:block)) + function new_usertype(name::Symbol) ::UserTypeTemplate end """ - `set_mutable!(::Expr, ::Bool = true) -> Nothing` + `set_mutable!(::UserTypeTemplate, ::Bool = true) -> Nothing` modify struct expressions mutability """ - function set_mutable!(expr::Expr, is_mutable::Bool = true) ::Nothing + function set_mutable!(proto::UserTypeTemplate, is_mutable::Bool = true) ::Nothing - expr.args[1] = is_mutable + proto._expr.args[1] = is_mutable return nothing end """ - `add_field!(::Expr, ::Symbol, ::Type = Any) -> Nothing` + `add_field!(::UserTypeTemplate, ::Symbol, ::Type = Any) -> Nothing` add explicitly types field to struct expression """ - function add_field!(expr::Expr, field_name::Symbol, type::Type = Any) ::Nothing + function add_field!(proto::UserTypeTemplate, field_name::Symbol, type::Type = Any) ::Nothing - push!(expr.args[3].args, Expr(:(::), field_name, Symbol(type))) + push!(proto._expr.args[3].args, Expr(:(::), field_name, Symbol(type))) return nothing end # overload to be used with symbols such as parameter names - function add_field!(expr::Expr, field_name::Symbol, type::Symbol) ::Nothing + function add_field!(proto::UserTypeTemplate, field_name::Symbol, type::Symbol) ::Nothing - push!(expr.args[3].args, Expr(:(::), field_name, type)) + push!(proto._expr.args[3].args, Expr(:(::), field_name, type)) return nothing end """ - `add_parameter(::Expr, ::Symbol, ::Type = Any) -> Nothing` + `add_parameter(::UserTypeTemplate, ::Symbol, ::Type = Any) -> Nothing` add parameters to struct expression """ - function add_parameter!(expr::Expr, param_name::Symbol, type::Type = Any) ::Nothing + function add_parameter!(proto::UserTypeTemplate, param_name::Symbol, type::Type = Any) ::Nothing + + proto._expr.args[2].args[1] = - if !(expr.args[2] isa Expr) - expr.args[2] = Expr(:curly, expr.args[2], Expr(:(<:), param_name, Symbol(Type))) + if !(proto._expr.args[2] isa Expr) + proto._expr.args[2] = Expr(:curly, proto._expr.args[2], Expr(:(<:), param_name, Symbol(Type))) else - push!(expr.args[2].args, Expr(:(<:), param_name, Symbol(Type))) + push!(proto._expr.args[2].args, Expr(:(<:), param_name, Symbol(Type))) end return nothing; end - + """ + `add_member_function(::UserTypeTemplate, <:Function) -> Nothing` + + add member function """ - function add_constructor!(expr::Expr, f::Function) ::Nothing - push!(expr.args[3].args, Expr(:(=), Expr(:call, :Test, ))) + function add_member_function!(proto::UserTypeTemplate, f::Function) ::Nothing + proto._member_functions[Symbol(f)] = f + return nothing; + end + + function add_member_function!(proto::UserTypeTemplate, name::Symbol, f::jluna._cppcall.UnnamedFunctionProxy) ::Nothing + proto._member_functions[name] = f + return nothing; end + """ + `implement(::UserTypeTemplate) -> UserType` + + evaluate usertype template and create an actual type from it + """ + function implement(template::UserTypeTemplate) ::UserType + + + end end end @@ -1044,4 +1086,13 @@ function cppcall(function_name::Symbol, xs...) ::Any return jluna._cppcall.get_result() end -export cppcall \ No newline at end of file +export cppcall + +""" +`(:)(::UserType, ::Symbol) -> Any` + +call member function of cpp usertype +""" +function (:)(type::jluna.usertype.UserType, function_name, xs...) #::Auto + return UserType._member_functions[function_name](xs...) +end \ No newline at end of file diff --git a/include/julia_extension.hpp b/include/julia_extension.hpp index dc13cbb..804bb88 100644 --- a/include/julia_extension.hpp +++ b/include/julia_extension.hpp @@ -145,6 +145,14 @@ extern "C" return jl_unbox_int64(jl_call1(length, value)); } + /// @brief return string as expression + inline jl_value_t* jl_quote(const char* in) + { + const std::string a = "quote "; + const std::string b = " end"; + return jl_eval_string((a + in + b).c_str()); + } + /// @brief pause gc and save current state #define jl_gc_pause bool _b_e_f_o_r_e_ = jl_gc_is_enabled(); jl_gc_enable(false); // weird naming to avoid potential name-collision when used in C++ diff --git a/include/usertype.hpp b/include/usertype.hpp index aba8deb..17b992a 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -15,6 +15,7 @@ namespace jluna class Usertype { public: + Usertype(const std::string& name); private: From b5d05f16e871ed1221bd6e6c3fb5bbd0ccbe36f2 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 02:06:10 +0100 Subject: [PATCH 03/56] usertype implement curly working --- include/jluna.jl | 178 ++++++++++++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 70 deletions(-) diff --git a/include/jluna.jl b/include/jluna.jl index 5e17f3c..30c2af0 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -954,107 +954,148 @@ module jluna module usertype - # template that can still be modified, transformed into actual type with `implement` - struct UserTypeTemplate + mutable struct UserType - _expr::Expr - _member_functions::Dict{Symbol, Union{Function, jluna._cppcall.UnnamedFunctionProxy}} + _name::Symbol - UserTypeTemplate(expr::Expr) = new(expr, Vector{Function}()) - end + _fields::Dict{Symbol, Any} + _const_fields::Dict{Symbol, Any} + _functions::Dict{Symbol, Union{Function}}#, jluna._cppcall.UnnamedFunctionProxy}} + _which::Dict{Symbol, Int8} + _parameters::Vector{TypeVar} - mutable struct UserType - _name::Symbol - _members::Dict{Symbol, Any} + UserType(name::Symbol) = new( + name, + Dict{Symbol, Any}(), + Dict{Symbol, Any}(), + Dict{Symbol, Union{Function}}(), + Dict{Symbol, Int8}(), + Vector{TypeVar}() + ) end - struct ImmutableUserType <: UserType + # exception when modifying const + mutable struct MutatingConstException <: Exception + _field_name::Symbol end + Base.showerror(io::IO, e::MutatingConstException) = print(io, "jluna.MutatingConstException: Trying to modify field :" * string(e._field_name) * " which was declared const") - mutable struct MutableUserType <: UserType + # exception when field does not exist + mutable struct MemberUndefinedException <: Exception + _type_name::Symbol + _field_name::Symbol end + Base.showerror(io::IO, e::MemberUndefinedException) = print(io, "jluna.MemberUndefinedException: UserType " * string(e._type_name) * " does not have member " * string(e._field_name)) - - """ - `new_struct(::Symbol) -> Expr` - """ - function new_usertype(name::Symbol) ::UserTypeTemplate + # new + function new_usertype(name::Symbol, enable_pretty_syntax::Bool = false) ::UserType + out = UserType(name) end + export new_usertype - """ - `set_mutable!(::UserTypeTemplate, ::Bool = true) -> Nothing` - - modify struct expressions mutability - """ - function set_mutable!(proto::UserTypeTemplate, is_mutable::Bool = true) ::Nothing + # params + function add_parameter!(type::UserType, symbol::Symbol, upper_bound::Type = Any{}, lower_bound::Type = Union{}) ::Nothing + push!(type._parameters, TypeVar(symbol, lower_bound, upper_bound)) + return nothing + end + export add_parameter! - proto._expr.args[1] = is_mutable + # add non-const field + function add_field!(type::UserType, symbol::Symbol, value) ::Nothing + type._fields[symbol] = value + type._which[symbol] = 1 return nothing end + export add_field! - """ - `add_field!(::UserTypeTemplate, ::Symbol, ::Type = Any) -> Nothing` - - add explicitly types field to struct expression - """ - function add_field!(proto::UserTypeTemplate, field_name::Symbol, type::Type = Any) ::Nothing - - push!(proto._expr.args[3].args, Expr(:(::), field_name, Symbol(type))) + # add const field + function add_const_field!(type::UserType, symbol::Symbol, value) ::Nothing + type._fields[symbol] = value + type._which[symbol] = 2 return nothing end + export add_const_field! - # overload to be used with symbols such as parameter names - function add_field!(proto::UserTypeTemplate, field_name::Symbol, type::Symbol) ::Nothing - - push!(proto._expr.args[3].args, Expr(:(::), field_name, type)) + # add member function + function add_member_function!(type::UserType, symbol::Symbol, f) ::Nothing + type._functions[symbol] = f + type._which[symbol] = 3 return nothing end + export add_member_function! - """ - `add_parameter(::UserTypeTemplate, ::Symbol, ::Type = Any) -> Nothing` + # get + function get_field(type::UserType, symbol::Symbol) ::Any - add parameters to struct expression - """ - function add_parameter!(proto::UserTypeTemplate, param_name::Symbol, type::Type = Any) ::Nothing + try + if type._which[symbol] == 1 + return type._fields[symbol] + elseif type._which[symbol] == 2 + return type._const_fields[symbol] + else + return type._functions[symbol] + end + catch e + throw(MemberUndefinedException(type._name, symbol)) + return nothing + end + end + export get_field + + # set + function set_field!(type::UserType, symbol::Symbol, value) ::Nothing - proto._expr.args[2].args[1] = + if !haskey(type._which, symbol) + throw(MemberUndefinedException(type._name, symbol)) + return nothing + end - if !(proto._expr.args[2] isa Expr) - proto._expr.args[2] = Expr(:curly, proto._expr.args[2], Expr(:(<:), param_name, Symbol(Type))) + if type._which[symbol] == 1 + type._fields[symbol] = value else - push!(proto._expr.args[2].args, Expr(:(<:), param_name, Symbol(Type))) + throw(MutatingConstException(symbol)) end - return nothing; - end - - """ - `add_member_function(::UserTypeTemplate, <:Function) -> Nothing` - - add member function - """ - function add_member_function!(proto::UserTypeTemplate, f::Function) ::Nothing - proto._member_functions[Symbol(f)] = f - return nothing; + return nothing end + export set_field! - function add_member_function!(proto::UserTypeTemplate, name::Symbol, f::jluna._cppcall.UnnamedFunctionProxy) ::Nothing - proto._member_functions[name] = f - return nothing; + # call f + function call_member_function(type::UserType, symbol::Symbol, xs...) ::Any + + if !haskey(type._which, symbol) + throw(MemberUndefinedException(type._name, symbol)) + return nothing + end + + if type._which[symbol] == 3 + return type._fields[symbol](xs...) + else + throw(UndefVarError(Symbol(string(type.name) * "." * string(symbol)))) + return nothing; + end end + export call_member_function - """ - `implement(::UserTypeTemplate) -> UserType` + # implement + function implement(type::UserType, m::Module = Main) ::Type - evaluate usertype template and create an actual type from it - """ - function implement(template::UserTypeTemplate) ::UserType + curly = Expr(:curly, type._name) + + for tv in type._parameters + push!(curly.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) + end + println(dump(:( + mutable struct $(curly) end + ))) end + export implement end + using Main.jluna.usertype end """ @@ -1088,11 +1129,8 @@ function cppcall(function_name::Symbol, xs...) ::Any end export cppcall -""" -`(:)(::UserType, ::Symbol) -> Any` -call member function of cpp usertype -""" -function (:)(type::jluna.usertype.UserType, function_name, xs...) #::Auto - return UserType._member_functions[function_name](xs...) -end \ No newline at end of file +#TODO +ut = jluna.new_usertype(:test) +jluna.add_parameter!(ut, :T, Any) +jluna.add_parameter!(ut, :U, AbstractFloat, Int) From ca85d3857e720c5e9dc5adde11c46e3d955d7933 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 02:13:07 +0100 Subject: [PATCH 04/56] stashing --- include/jluna.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/include/jluna.jl b/include/jluna.jl index 30c2af0..111930d 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1083,13 +1083,18 @@ module jluna function implement(type::UserType, m::Module = Main) ::Type curly = Expr(:curly, type._name) - for tv in type._parameters push!(curly.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) end + + println(dump(:( - mutable struct $(curly) end + mutable struct $(curly) + + __ut::UserType + $(type.name)(x::UserType) = new( + end ))) end From ade7d310258f47b4d0e2384486c754c49cfee70d Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 19:17:04 +0100 Subject: [PATCH 05/56] implement cpp interface for usertype --- .src/state.cpp | 1 + .src/usertype.inl | 87 ++++++++++++++++++++++++++++++++++++++++++++ .test/main.cpp | 3 +- CMakeLists.txt | 5 ++- include/jluna.jl | 9 +++-- include/type.hpp | 1 + include/usertype.hpp | 42 +++++++++++++++++---- jluna.hpp | 3 +- 8 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 .src/usertype.inl diff --git a/.src/state.cpp b/.src/state.cpp index 34a341c..74a6be1 100644 --- a/.src/state.cpp +++ b/.src/state.cpp @@ -285,6 +285,7 @@ namespace jluna::State::detail UndefInitializer_t = Type(unroll("Core.UndefInitializer")); Union_t = Type(unroll("Core.Union")); UnionAll_t = Type(unroll("Core.UnionAll")); + UnionEmpty_t = Type((jl_datatype_t*) jl_eval_string("return Union{}")); Unsigned_t = Type(unroll("Core.Unsigned")); VecElement_t = Type(unroll("Core.VecElement")); WeakRef_t = Type(unroll("Core.WeakRef")); diff --git a/.src/usertype.inl b/.src/usertype.inl new file mode 100644 index 0000000..4cd32a4 --- /dev/null +++ b/.src/usertype.inl @@ -0,0 +1,87 @@ +// +// Copyright 2022 Clemens Cords +// Created on 25.02.22 by clem (mail@clemens-cords.com) +// + +#include + +namespace jluna +{ + template + UserType::UserType(const std::string& name) + { + static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); + + jl_gc_pause; + _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str())), nullptr); + jl_gc_unpause; + } + + template + void UserType::add_field(const std::string& name, Type type, Any* initial_value) + { + jl_gc_pause; + static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), initial_value); + jl_gc_unpause; + } + + template + template, Bool>> + void UserType::add_field(const std::string& name, Type type, U initial_value) + { + UserType::add_field(name, type, box(initial_value)); + } + + template + void UserType::add_const_field(const std::string& name, Type type, Any* initial_value) + { + jl_gc_pause; + static jl_function_t* add_const_field = jl_find_function("jluna.usertype", "add_const_field!"); + jluna::safe_call(add_const_field, _template, jl_symbol(name.c_str()), initial_value); + jl_gc_unpause; + } + + template + template, Bool>> + void UserType::add_const_field(const std::string& name, Type type, U initial_value) + { + UserType::add_const_field(name, type, box(initial_value)); + } + + template + template, Bool>> + void UserType::add_function(const std::string& name, Lambda_t lambda) + { + UserType::add_function(name, box(lambda)); + } + + template + void UserType::add_function(const std::string& name, Any* f) + { + jl_gc_pause; + static jl_function_t* add_member_function = jl_find_function("jluna.usertype", "add_member_function!"); + jluna::safe_call(add_member_function, f); + jl_gc_unpause; + } + + template + void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) + { + jl_gc_pause; + static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); + jluna::safe_call(add_parameter, _template, jl_symbol(name.c_str()), upper_bound, lower_bound); + jl_gc_unpause; + } + + template + Type UserType::implement(Module module) + { + jl_gc_pause; + static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); + auto out = Type((jl_datatype_t*) jluna::safe_call(implement, _template, module)); + jl_gc_unpause; + + return out; + } +} \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 935f072..70e88b5 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -13,9 +13,8 @@ using namespace jluna::detail; int main() { State::initialize(); + auto ut = UserType("test"); - jluna::safe_eval("throw(ErrorException(\"abc\"))"); -return 0; Test::initialize(); Test::test("catch c exception", [](){ diff --git a/CMakeLists.txt b/CMakeLists.txt index 974088d..242c5ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,10 @@ add_library(jluna SHARED include/generator_expression.hpp .src/generator_expression.cpp - include/usertype.hpp include/inline_eval.hpp) + + include/usertype.hpp + .src/usertype.inl +) set_target_properties(jluna PROPERTIES LINKER_LANGUAGE C diff --git a/include/jluna.jl b/include/jluna.jl index 111930d..c5ba8f7 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -990,7 +990,7 @@ module jluna Base.showerror(io::IO, e::MemberUndefinedException) = print(io, "jluna.MemberUndefinedException: UserType " * string(e._type_name) * " does not have member " * string(e._field_name)) # new - function new_usertype(name::Symbol, enable_pretty_syntax::Bool = false) ::UserType + function new_usertype(name::Symbol) ::UserType out = UserType(name) end export new_usertype @@ -1089,14 +1089,15 @@ module jluna + + println(dump(:( mutable struct $(curly) - __ut::UserType - $(type.name)(x::UserType) = new( + #__ut::UserType + #$(type.name)(x::UserType) = new end ))) - end export implement end diff --git a/include/type.hpp b/include/type.hpp index d5907f9..628209b 100644 --- a/include/type.hpp +++ b/include/type.hpp @@ -200,6 +200,7 @@ namespace jluna inline Type UndefInitializer_t; inline Type Union_t; inline Type UnionAll_t; + inline Type UnionEmpty_t; inline Type Unsigned_t; inline Type VecElement_t; inline Type WeakRef_t; diff --git a/include/usertype.hpp b/include/usertype.hpp index 17b992a..3e581fc 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -7,18 +7,46 @@ #include -namespace jluna -{ - auto* a = (jl_datatype_t*) jl_array_type; +#include +#include - class Usertype +namespace jluna +{ + /// @brief customizable wrapper for non-julia type T + template + class UserType { public: - Usertype(const std::string& name); + /// @brief ctor + /// @param julia_side_name: exact name of the resulting julia type + UserType(const std::string& julia_side_name); + + /// @brief add field + template, Bool> = true> + void add_field(const std::string& name, Type type, Value_t initial_value); + void add_field(const std::string& name, Type type = Any_t, Any* initial_value = jl_undef_initializer()); + /// @brief add const field + template, Bool> = true> + void add_const_field(const std::string& name, Type type, Value_t initial_value); + void add_const_field(const std::string& name, Type type = Any_t, Any* initial_value = jl_undef_initializer()); + + /// @brief add member function + template, Bool> = true> + void add_function(const std::string& name, Lambda_t lambda); + void add_function(const std::string& name, Any* function); + + /// @brief add parameter + void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + + /// @brief push to state and eval, cannot be extended afterwards + Type implement(Module module = Main); private: - Any* _expression; + Proxy _template; + jl_datatype_t* _type = nullptr; }; -} \ No newline at end of file +} + +#include ".src/usertype.inl" \ No newline at end of file diff --git a/jluna.hpp b/jluna.hpp index a312470..2b02481 100644 --- a/jluna.hpp +++ b/jluna.hpp @@ -19,4 +19,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file From 90d8cf4526cf7b8d80fe71cf433a607bad81a2ac Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 22:29:15 +0100 Subject: [PATCH 06/56] reimplementing usertypes --- include/jluna.jl | 133 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 112 insertions(+), 21 deletions(-) diff --git a/include/jluna.jl b/include/jluna.jl index c5ba8f7..9247f4e 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -959,7 +959,6 @@ module jluna _name::Symbol _fields::Dict{Symbol, Any} - _const_fields::Dict{Symbol, Any} _functions::Dict{Symbol, Union{Function}}#, jluna._cppcall.UnnamedFunctionProxy}} _which::Dict{Symbol, Int8} @@ -969,7 +968,6 @@ module jluna UserType(name::Symbol) = new( name, Dict{Symbol, Any}(), - Dict{Symbol, Any}(), Dict{Symbol, Union{Function}}(), Dict{Symbol, Int8}(), Vector{TypeVar}() @@ -1010,14 +1008,6 @@ module jluna end export add_field! - # add const field - function add_const_field!(type::UserType, symbol::Symbol, value) ::Nothing - type._fields[symbol] = value - type._which[symbol] = 2 - return nothing - end - export add_const_field! - # add member function function add_member_function!(type::UserType, symbol::Symbol, f) ::Nothing type._functions[symbol] = f @@ -1080,24 +1070,34 @@ module jluna export call_member_function # implement - function implement(type::UserType, m::Module = Main) ::Type + function implement(type::UserType, m::Module = Main) ::Expr - curly = Expr(:curly, type._name) + parameters = Expr(:curly, type._name) for tv in type._parameters - push!(curly.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) + if tv.lb == Union{} + if tv.ub == Any + push!(param.args, tv.name) + else + push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) + end + else + push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) + end end + block = Expr(:block) + for (s, t) in type._fields + push!(block.args, Expr(:(::), s, t)) + end + for (s, t) in type._functions + push!(block.args, Expr(:(::), s, Symbol("Function"))) + end + out::Expr = :(mutable struct $(parameters) end) + out.args[3] = block - - println(dump(:( - mutable struct $(curly) - - #__ut::UserType - #$(type.name)(x::UserType) = new - end - ))) + return out end export implement end @@ -1140,3 +1140,94 @@ export cppcall ut = jluna.new_usertype(:test) jluna.add_parameter!(ut, :T, Any) jluna.add_parameter!(ut, :U, AbstractFloat, Int) +jluna.add_field!(ut, :_field01, Any) +jluna.add_field!(ut, :_field02, Integer) +jluna.add_member_function!(ut, :f, x -> println(x)) + +mutable struct UserType + + _name::Symbol + _is_mutable::Bool + _fields::Dict{Symbol, Any} + _parameters::Vector{TypeVar} + + UserType(name::Symbol, is_mutable::Bool = true) = new( + name, + is_mutable, + Dict{Symbol, Any}(), + Vector{TypeVar}() + ) +end + +function new_usertype(name::Symbol) + return UserType(name) +end + +function set_mutable!(x::UserType, value::Bool) ::Nothing + x._is_mutable = value + return nothing +end + +function add_field!(x::UserType, name::Symbol, value) ::Nothing + x._fields[name] = value +end + +function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) + push!(x._parameters[name], TypeVar(name, lb, ub)) +end + +function implement(type::UserType) + + # params and name + parameters = Expr(:curly, type._name) + for tv in type._parameters + if tv.lb == Union{} + if tv.ub == Any + push!(param.args, tv.name) + else + push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) + end + else + push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) + end + end + + block = Expr(:block) + + # fields + for (field_name, field_type) in type._fields + push!(block.args, Expr(:(::), field_name, field_type)) + end + + # ctor + + + + out::Expr = :(mutable struct $(parameters) end) + out.args[3] = block + + return out +end + +struct MyType + + _field01 + _field02 + + MyType(base::UserType) = new( + base._fields[:_field01], + base._fields[:_field02] + ) +end + +instance = UserType() +instance.set_field(instance, :_field01, 1234) +instance.set_field(instance, :_field02, 4567) +instance.set_field(instance, :f, x -> println(x)) + + + + + + + From 4028bc429410f889c2a0b36f7b31f62de189acdd Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 22:41:06 +0100 Subject: [PATCH 07/56] stashing --- include/jluna.jl | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/include/jluna.jl b/include/jluna.jl index 9247f4e..e9a2de0 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1170,15 +1170,16 @@ end function add_field!(x::UserType, name::Symbol, value) ::Nothing x._fields[name] = value + return nothing end -function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) - push!(x._parameters[name], TypeVar(name, lb, ub)) +function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing + push!(x._parameters, TypeVar(name, lb, ub)) + return nothing end function implement(type::UserType) - # params and name parameters = Expr(:curly, type._name) for tv in type._parameters if tv.lb == Union{} @@ -1194,15 +1195,13 @@ function implement(type::UserType) block = Expr(:block) - # fields - for (field_name, field_type) in type._fields - push!(block.args, Expr(:(::), field_name, field_type)) + for (field_name, field_value) in type._fields + push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) end - # ctor - - + ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:block)) + push!(block.args, ctor) out::Expr = :(mutable struct $(parameters) end) out.args[3] = block @@ -1220,10 +1219,13 @@ struct MyType ) end -instance = UserType() -instance.set_field(instance, :_field01, 1234) -instance.set_field(instance, :_field02, 4567) -instance.set_field(instance, :f, x -> println(x)) +instance = UserType(:TestType) +add_parameter!(instance, :T, Integer) +set_mutable!(instance, true) +add_field!(instance, :_field01, 1234) +add_field!(instance, :_field02, 4567) +add_field!(instance, :f, x -> println(x)) +implement(instance) From c1c3d9cdd88f8ab8d48be1ac40358a994534c571 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 23:19:34 +0100 Subject: [PATCH 08/56] stashing --- include/jluna.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/include/jluna.jl b/include/jluna.jl index e9a2de0..45058de 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1199,7 +1199,17 @@ function implement(type::UserType) push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) end - ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:block)) + new_and_curly = Expr(:curly, :new) + for tv in type._parameters + push!(new_and_curly.args, tv.name) + end + + ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:call, new_and_curly)) + + for (field_name, _) in type._fields + field_symbol = QuoteNode(field_name) + push!(ctor.args[2].args, :(base._fields[$(field_symbol)])) + end push!(block.args, ctor) out::Expr = :(mutable struct $(parameters) end) From 3e00d05b57dee7baa77e2110ffe3d8d22aea3e60 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 25 Feb 2022 23:35:16 +0100 Subject: [PATCH 09/56] working --- include/jluna.jl | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/include/jluna.jl b/include/jluna.jl index 45058de..8ede3d3 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1199,12 +1199,33 @@ function implement(type::UserType) push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) end - new_and_curly = Expr(:curly, :new) - for tv in type._parameters - push!(new_and_curly.args, tv.name) - end + ctor::Expr = :() + + if isempty(type._parameters) + ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:call, :new)) + else - ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:call, new_and_curly)) + curly_new = Expr(:curly, :new); + for t in type._parameters + push!(curly_new.args, t.name) + end + + where_call = Expr( + :where, + Expr( + :call, + Expr( + :curly, + type._name, + (collect(p.name for p in type._parameters)...) + ), + :(base::UserType) + ), + (collect(p.name for p in type._parameters)...) + ) + + ctor = Expr(:(=), where_call, Expr(:call, curly_new)) + end for (field_name, _) in type._fields field_symbol = QuoteNode(field_name) From 5cd2e8277090a78d8a3b93314fb86b055693bb2c Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 00:12:56 +0100 Subject: [PATCH 10/56] stashing --- .src/usertype.inl | 3 +- .test/main.cpp | 23 +++- include/jluna.jl | 309 ++++++++++++------------------------------- include/usertype.hpp | 4 +- 4 files changed, 113 insertions(+), 226 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 4cd32a4..6c54897 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -8,7 +8,8 @@ namespace jluna { template - UserType::UserType(const std::string& name) + template + UserType::UserType(const std::string& name, Lambda_t lambda) { static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); diff --git a/.test/main.cpp b/.test/main.cpp index 70e88b5..145a3db 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -10,10 +10,31 @@ using namespace jluna; using namespace jluna::detail; +template +struct NonJuliaType +{ + inline NonJuliaType(T in) + : _member_var(in) + {} + + T _member_var; + + int member_function() + { + return _member_var * _member_var; + } +}; + int main() { State::initialize(); - auto ut = UserType("test"); + auto ut = UserType>("NonJuliaType", [](){ + + }); + + + + return 0; Test::initialize(); diff --git a/include/jluna.jl b/include/jluna.jl index 8ede3d3..e3c8510 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -952,125 +952,79 @@ module jluna end end + """ + interface for jluna::UserType + """ module usertype + """ + usertype wrapper + """ mutable struct UserType _name::Symbol - + _is_mutable::Bool _fields::Dict{Symbol, Any} - _functions::Dict{Symbol, Union{Function}}#, jluna._cppcall.UnnamedFunctionProxy}} - _which::Dict{Symbol, Int8} - _parameters::Vector{TypeVar} - - UserType(name::Symbol) = new( + UserType(name::Symbol, is_mutable::Bool = true) = new( name, + is_mutable, Dict{Symbol, Any}(), - Dict{Symbol, Union{Function}}(), - Dict{Symbol, Int8}(), Vector{TypeVar}() ) end + export UserType - # exception when modifying const - mutable struct MutatingConstException <: Exception - _field_name::Symbol - end - Base.showerror(io::IO, e::MutatingConstException) = print(io, "jluna.MutatingConstException: Trying to modify field :" * string(e._field_name) * " which was declared const") - - # exception when field does not exist - mutable struct MemberUndefinedException <: Exception - _type_name::Symbol - _field_name::Symbol - end - Base.showerror(io::IO, e::MemberUndefinedException) = print(io, "jluna.MemberUndefinedException: UserType " * string(e._type_name) * " does not have member " * string(e._field_name)) + """ + `new_usertype(::Symbol) -> UserType` - # new + create new usertype + """ function new_usertype(name::Symbol) ::UserType - out = UserType(name) + return UserType(name) end export new_usertype - # params - function add_parameter!(type::UserType, symbol::Symbol, upper_bound::Type = Any{}, lower_bound::Type = Union{}) ::Nothing - push!(type._parameters, TypeVar(symbol, lower_bound, upper_bound)) - return nothing - end - export add_parameter! - - # add non-const field - function add_field!(type::UserType, symbol::Symbol, value) ::Nothing - type._fields[symbol] = value - type._which[symbol] = 1 - return nothing - end - export add_field! + """ + `set_mutable(::UserType, ::Bool) -> Nothing` - # add member function - function add_member_function!(type::UserType, symbol::Symbol, f) ::Nothing - type._functions[symbol] = f - type._which[symbol] = 3 + change mutability of usertype + """ + function set_mutable!(x::UserType, value::Bool) ::Nothing + x._is_mutable = value return nothing end - export add_member_function! - - # get - function get_field(type::UserType, symbol::Symbol) ::Any - - try - if type._which[symbol] == 1 - return type._fields[symbol] - elseif type._which[symbol] == 2 - return type._const_fields[symbol] - else - return type._functions[symbol] - end - catch e - throw(MemberUndefinedException(type._name, symbol)) - return nothing - end - end - export get_field + export set_mutable! - # set - function set_field!(type::UserType, symbol::Symbol, value) ::Nothing - - if !haskey(type._which, symbol) - throw(MemberUndefinedException(type._name, symbol)) - return nothing - end - - if type._which[symbol] == 1 - type._fields[symbol] = value - else - throw(MutatingConstException(symbol)) - end + """ + `add_field!(::UserType, ::Symbol, value) -> Nothing` + add field to usertype, can also be a function + """ + function add_field!(x::UserType, name::Symbol, value) ::Nothing + x._fields[name] = value return nothing end - export set_field! - - # call f - function call_member_function(type::UserType, symbol::Symbol, xs...) ::Any + export add_field! - if !haskey(type._which, symbol) - throw(MemberUndefinedException(type._name, symbol)) - return nothing - end + """ + `add_parameter!(::UserType, ::Symbol, upper_bound::Type, lower_bound::Type) -> Nothing` - if type._which[symbol] == 3 - return type._fields[symbol](xs...) - else - throw(UndefVarError(Symbol(string(type.name) * "." * string(symbol)))) - return nothing; - end + add parameter, including upper and lower bounds + """ + function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing + push!(x._parameters, TypeVar(name, lb, ub)) + return nothing end - export call_member_function + export add_parameter! - # implement - function implement(type::UserType, m::Module = Main) ::Expr + """ + `implement(::UserType) -> Type` + + evaluate the type + """ + function implement(type::UserType, m::Module = Main) ::Type parameters = Expr(:curly, type._name) for tv in type._parameters @@ -1086,18 +1040,49 @@ module jluna end block = Expr(:block) - for (s, t) in type._fields - push!(block.args, Expr(:(::), s, t)) + + for (field_name, field_value) in type._fields + push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) + end + + ctor::Expr = :() + + if isempty(type._parameters) + ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.UserType)), Expr(:call, :new)) + else + curly_new = Expr(:curly, :new); + for t in type._parameters + push!(curly_new.args, t.name) + end + + where_call = Expr( + :where, + Expr( + :call, + Expr( + :curly, + type._name, + (collect(p.name for p in type._parameters)...) + ), + :(base::jluna.usertype.UserType) + ), + (collect(p.name for p in type._parameters)...) + ) + + ctor = Expr(:(=), where_call, Expr(:call, curly_new)) end - for (s, t) in type._functions - push!(block.args, Expr(:(::), s, Symbol("Function"))) + for (field_name, _) in type._fields + field_symbol = QuoteNode(field_name) + push!(ctor.args[2].args, :(base._fields[$(field_symbol)])) end + push!(block.args, ctor) out::Expr = :(mutable struct $(parameters) end) out.args[3] = block - return out + Base.eval(m, out); + return Base.eval(m, type._name) end export implement end @@ -1135,132 +1120,10 @@ function cppcall(function_name::Symbol, xs...) ::Any end export cppcall - -#TODO -ut = jluna.new_usertype(:test) -jluna.add_parameter!(ut, :T, Any) -jluna.add_parameter!(ut, :U, AbstractFloat, Int) -jluna.add_field!(ut, :_field01, Any) -jluna.add_field!(ut, :_field02, Integer) -jluna.add_member_function!(ut, :f, x -> println(x)) - -mutable struct UserType - - _name::Symbol - _is_mutable::Bool - _fields::Dict{Symbol, Any} - _parameters::Vector{TypeVar} - - UserType(name::Symbol, is_mutable::Bool = true) = new( - name, - is_mutable, - Dict{Symbol, Any}(), - Vector{TypeVar}() - ) -end - -function new_usertype(name::Symbol) - return UserType(name) -end - -function set_mutable!(x::UserType, value::Bool) ::Nothing - x._is_mutable = value - return nothing -end - -function add_field!(x::UserType, name::Symbol, value) ::Nothing - x._fields[name] = value - return nothing -end - -function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing - push!(x._parameters, TypeVar(name, lb, ub)) - return nothing -end - -function implement(type::UserType) - - parameters = Expr(:curly, type._name) - for tv in type._parameters - if tv.lb == Union{} - if tv.ub == Any - push!(param.args, tv.name) - else - push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) - end - else - push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) - end - end - - block = Expr(:block) - - for (field_name, field_value) in type._fields - push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) - end - - ctor::Expr = :() - - if isempty(type._parameters) - ctor = Expr(:(=), :($(type._name)(base::UserType)), Expr(:call, :new)) - else - - curly_new = Expr(:curly, :new); - for t in type._parameters - push!(curly_new.args, t.name) - end - - where_call = Expr( - :where, - Expr( - :call, - Expr( - :curly, - type._name, - (collect(p.name for p in type._parameters)...) - ), - :(base::UserType) - ), - (collect(p.name for p in type._parameters)...) - ) - - ctor = Expr(:(=), where_call, Expr(:call, curly_new)) - end - - for (field_name, _) in type._fields - field_symbol = QuoteNode(field_name) - push!(ctor.args[2].args, :(base._fields[$(field_symbol)])) - end - - push!(block.args, ctor) - out::Expr = :(mutable struct $(parameters) end) - out.args[3] = block - - return out -end - -struct MyType - - _field01 - _field02 - - MyType(base::UserType) = new( - base._fields[:_field01], - base._fields[:_field02] - ) -end - -instance = UserType(:TestType) -add_parameter!(instance, :T, Integer) -set_mutable!(instance, true) -add_field!(instance, :_field01, 1234) -add_field!(instance, :_field02, 4567) -add_field!(instance, :f, x -> println(x)) -implement(instance) - - - - - - - +instance = jluna.usertype.UserType(:TestType) +jluna.usertype.add_parameter!(instance, :T, Integer) +jluna.usertype.set_mutable!(instance, true) +jluna.usertype.add_field!(instance, :_field01, 1234) +jluna.usertype.add_field!(instance, :_field02, 4567) +jluna.usertype.add_field!(instance, :f, x -> println(x)) +jluna.usertype.implement(instance) \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 3e581fc..a20517e 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -20,7 +20,8 @@ namespace jluna public: /// @brief ctor /// @param julia_side_name: exact name of the resulting julia type - UserType(const std::string& julia_side_name); + template + UserType(const std::string& julia_side_name, Lambda_t boxing_routine); /// @brief add field template, Bool> = true> @@ -44,6 +45,7 @@ namespace jluna Type implement(Module module = Main); private: + Proxy _template; jl_datatype_t* _type = nullptr; }; From 2c91dcea4f24a87e8e415d8fccadc00193c90ace Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 02:30:56 +0100 Subject: [PATCH 11/56] c++ interface declared --- include/usertype.hpp | 142 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 21 deletions(-) diff --git a/include/usertype.hpp b/include/usertype.hpp index a20517e..f574df2 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -7,47 +7,147 @@ #include - #include #include namespace jluna { + template + struct UserTypeAlreadyImplementedException; + + template + struct UserTypeNotFullyImplementedException; + /// @brief customizable wrapper for non-julia type T + /// @note for information on how to use this class, visit https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes template class UserType { public: - /// @brief ctor - /// @param julia_side_name: exact name of the resulting julia type - template - UserType(const std::string& julia_side_name, Lambda_t boxing_routine); + /// ### STATIC INTERFACE ################################ - /// @brief add field - template, Bool> = true> - void add_field(const std::string& name, Type type, Value_t initial_value); - void add_field(const std::string& name, Type type = Any_t, Any* initial_value = jl_undef_initializer()); + /// @brief original type + using original_type = T; - /// @brief add const field - template, Bool> = true> - void add_const_field(const std::string& name, Type type, Value_t initial_value); - void add_const_field(const std::string& name, Type type = Any_t, Any* initial_value = jl_undef_initializer()); + /// @brief set julia-side name + /// @param name + static void set_name(const std::string& name); - /// @brief add member function - template, Bool> = true> - void add_function(const std::string& name, Lambda_t lambda); - void add_function(const std::string& name, Any* function); + /// @brief get julia-side name + /// @returns name + static const std::string& get_name(); + + /// @brief set mutability, no by default + /// @param bool + static void set_mutable(bool); + + /// @brief get mutability + /// @returns bool + static bool is_mutable(); + + /// @brief add field + /// @param name of field + /// @param type of field + /// @param initial value + template + static void add_field(const std::string& name, Type type, Value_t initial_value); /// @brief add parameter - void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + /// @param name: e.g. T + /// @param upper_bound: .ub of TypeVar, equivalent to T <: upper_bound + /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: T + static void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + + /// @brief boxing routine called during box> + /// @param: lambda + /// @note c.f. https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes + static void set_boxing_routine(std::function lambda); + + /// @brief boxing routine called during unbox> + /// @param: lambda + /// @note c.f. https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes + static void set_boxing_routine(std::function lambda); /// @brief push to state and eval, cannot be extended afterwards - Type implement(Module module = Main); + /// @param module: module the type will be set in + /// @returns julia-side type + static Type implement(Module module = Main); + + /// @brief is already implemented + /// @brief true if implement was called, false otherwise + static bool is_implemented(); + + /// @brief is fully initialized + /// @returns true if boxing, unboxing and name was set, false otherwise + static bool is_initialized(); + + /// @brief no ctor + UserType() = delete; + + /// @brief instance this type using an original + /// @param cpp_side_type + /// @returns proxy to new instance + static Proxy create_instance(T); private: + static inline bool _already_initialized = false; + static inline bool _already_implemented = false; + static void pre_initialize(); + + static inline Proxy _template = Proxy(jl_nothing); + }; + + /// @brief unbox using unboxing routine + /// @param pointer + /// @returns T + template>, Bool> = true> + inline T unbox(Any* in) + { + if(not T::is_implemented() or not T::is_initialized()) + throw UserTypeNotFullyImplementedException(); + + return T::unboxing_routine(in); + } + + /// @brief box using boxing routine + /// @param + /// @returns pointer to julia-side memory + template>, Bool> = true> + inline Any* box(T in) + { + if(not T::is_implemented() or not T::is_initialized()) + throw UserTypeNotFullyImplementedException(); + + return T::boxing_routine(in); + } + + /// @brief exception raised if usertype is implemented again + template + struct UserTypeAlreadyImplementedException : public std::exception + { + /// @brief ctor + /// @param name + UserTypeAlreadyImplementedException(); + + /// @brief what + /// @returns message + virtual const char* what() const noexcept override final; + }; + + template + struct UserTypeNotFullyImplementedException : public std::exception + { + /// @brief ctor + /// @param name + UserTypeNotFullyImplementedException(); - Proxy _template; - jl_datatype_t* _type = nullptr; + /// @brief what + /// @returns message + virtual const char* what() const noexcept override final; }; } From fac2663ed935dff6fecf6fd9f8c6e452700b5f7e Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 19:09:07 +0100 Subject: [PATCH 12/56] usertype implemented --- .src/usertype.inl | 129 ++++++++++++++++++++++++++++++++----------- include/usertype.hpp | 44 +++++++-------- 2 files changed, 117 insertions(+), 56 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 6c54897..c429eec 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -8,81 +8,148 @@ namespace jluna { template - template - UserType::UserType(const std::string& name, Lambda_t lambda) + UserTypeNotFullyInitializedException::UserTypeNotFullyInitializedException() + {} + + template + const char * UserTypeNotFullyInitializedException::what() const noexcept { - static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); - - jl_gc_pause; - _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str())), nullptr); - jl_gc_unpause; + std::stringstream str; + str << "[C++][EXCEPTION] UserType interface for this type has not yet been fully specified, make sure that the following actions were performed:" << std::endl; + str << "\ta) UserType::set_name was used to specify the name" << std::endl; + str << "\tb) UserType::set_boxing_routine was used to implement the boxing routine" << std::endl; + str << "\tc) UserType::set_unboxing_routine was used to implement the unboxing routine" << std::endl; + str << "\td) after all of the above, UserType::implement was called exactly once" << std::endl; + return str.str.c_str(); } - + template - void UserType::add_field(const std::string& name, Type type, Any* initial_value) + void UserType::pre_initialize() { + if (_already_pre_initialized) + return; + + throw_if_uninitialized(); jl_gc_pause; - static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); - jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), initial_value); + _template = Proxy(jl_eval_string("return jluna.usertype.new_usertype(:unitialized)")); + + _boxing_routine = [](T _) -> Any* { + throw UserTypeNotFullyInitializedException(); + return jl_nothing; + }; + + _unboxing_routine = [](Any* _) -> T { + throw UserTypeNotFullyInitializedException(); + return; + }; + jl_gc_unpause; } template - template, Bool>> - void UserType::add_field(const std::string& name, Type type, U initial_value) + void UserType::set_name(const std::string& name) { - UserType::add_field(name, type, box(initial_value)); + pre_initialize(); + _template["_name"] = jl_symbol(name.c_str()); + _name_set = true; } template - void UserType::add_const_field(const std::string& name, Type type, Any* initial_value) + std::string UserType::get_name() { - jl_gc_pause; - static jl_function_t* add_const_field = jl_find_function("jluna.usertype", "add_const_field!"); - jluna::safe_call(add_const_field, _template, jl_symbol(name.c_str()), initial_value); - jl_gc_unpause; + pre_initialize(); + return _template["_name"].operator std::string(); } template - template, Bool>> - void UserType::add_const_field(const std::string& name, Type type, U initial_value) + void UserType::set_mutable(bool b) { - UserType::add_const_field(name, type, box(initial_value)); + pre_initialize(); + _template["_is_mutable"] = b; } template - template, Bool>> - void UserType::add_function(const std::string& name, Lambda_t lambda) + bool UserType::is_mutable() { - UserType::add_function(name, box(lambda)); + pre_initialize(); + return _template["_is_mutable"] = b; } template - void UserType::add_function(const std::string& name, Any* f) + template + void UserType::add_field(const std::string& name, Value_t initial_value) { + pre_initialize(); jl_gc_pause; - static jl_function_t* add_member_function = jl_find_function("jluna.usertype", "add_member_function!"); - jluna::safe_call(add_member_function, f); + static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), box(initial_value)); jl_gc_unpause; } template void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) { + pre_initialize(); jl_gc_pause; static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); - jluna::safe_call(add_parameter, _template, jl_symbol(name.c_str()), upper_bound, lower_bound); + jluna::safe_call(add_parameter,_template, jl_symbol(name.c_str()), upper_bound, lower_bound); jl_gc_unpause; } template - Type UserType::implement(Module module) + void UserType::set_boxing_routine(std::function lambda) { + pre_initialize(); + + _boxing_routine = lamdba; + _boxing_routine_set = true; + } + + template + void UserType::set_unboxing_routine(std::function lambda) + { + pre_initialize(); + + _unboxing_routine = lambda; + _unboxing_routine_set = true; + } + + template + void UserType::implement(Module module) + { + pre_initialize(); + + if (_implemented) + throw UserTypeAlreadyImplementedException(); + + if (not is_initialized()) + throw UserTypeNotFullyInitializedException(); + jl_gc_pause; static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); - auto out = Type((jl_datatype_t*) jluna::safe_call(implement, _template, module)); + jluna::safe_call(implement, _template, module); + _implemented = true; jl_gc_unpause; + } + + template + bool UserType::is_implemented() + { + return _implemented; + } + + template + bool UserType::is_initialized() + { + return _boxing_routine_set && _unboxing_routine_set && _name_set; + } + template + static Proxy UserType::create_instance(T in) + { + jl_gc_pause; + auto out = Proxy(box(in)); + jl_gc_unpause; return out; } } \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index f574df2..a2e1301 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -16,7 +16,7 @@ namespace jluna struct UserTypeAlreadyImplementedException; template - struct UserTypeNotFullyImplementedException; + struct UserTypeNotFullyInitializedException; /// @brief customizable wrapper for non-julia type T /// @note for information on how to use this class, visit https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes @@ -24,8 +24,6 @@ namespace jluna class UserType { public: - /// ### STATIC INTERFACE ################################ - /// @brief original type using original_type = T; @@ -35,7 +33,7 @@ namespace jluna /// @brief get julia-side name /// @returns name - static const std::string& get_name(); + static std::string get_name(); /// @brief set mutability, no by default /// @param bool @@ -50,7 +48,7 @@ namespace jluna /// @param type of field /// @param initial value template - static void add_field(const std::string& name, Type type, Value_t initial_value); + static void add_field(const std::string& name, Value_t initial_value); /// @brief add parameter /// @param name: e.g. T @@ -66,7 +64,7 @@ namespace jluna /// @brief boxing routine called during unbox> /// @param: lambda /// @note c.f. https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes - static void set_boxing_routine(std::function lambda); + static void set_unboxing_routine(std::function lambda); /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in @@ -90,11 +88,20 @@ namespace jluna static Proxy create_instance(T); private: - static inline bool _already_initialized = false; - static inline bool _already_implemented = false; static void pre_initialize(); + static inline bool _pre_initialized = false; static inline Proxy _template = Proxy(jl_nothing); + + static std::function _boxing_routine; + static inline bool _boxing_routine_set = false; + + static std::function _unboxing_routine; + static inline bool _unboxing_routine_set = false; + + bool _name_set = false; + bool _implemented = false; + }; /// @brief unbox using unboxing routine @@ -106,7 +113,7 @@ namespace jluna inline T unbox(Any* in) { if(not T::is_implemented() or not T::is_initialized()) - throw UserTypeNotFullyImplementedException(); + throw UserTypeNotFullyInitializedException(); return T::unboxing_routine(in); } @@ -120,30 +127,17 @@ namespace jluna inline Any* box(T in) { if(not T::is_implemented() or not T::is_initialized()) - throw UserTypeNotFullyImplementedException(); + throw UserTypeNotFullyInitializedException(); return T::boxing_routine(in); } - /// @brief exception raised if usertype is implemented again - template - struct UserTypeAlreadyImplementedException : public std::exception - { - /// @brief ctor - /// @param name - UserTypeAlreadyImplementedException(); - - /// @brief what - /// @returns message - virtual const char* what() const noexcept override final; - }; - template - struct UserTypeNotFullyImplementedException : public std::exception + struct UserTypeNotFullyInitializedException : public std::exception { /// @brief ctor /// @param name - UserTypeNotFullyImplementedException(); + UserTypeNotFullyInitializedException(); /// @brief what /// @returns message From 795e8291f491116f733edeb1c4d378f0ce3ca636 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 19:41:57 +0100 Subject: [PATCH 13/56] stashing --- .src/state.cpp | 2 +- .src/usertype.inl | 29 ++++++++++------------------- .test/main.cpp | 16 ++++++++-------- CMakeLists.txt | 2 +- include/jluna.jl | 8 +++++++- include/usertype.hpp | 5 ++--- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.src/state.cpp b/.src/state.cpp index 74a6be1..19811ca 100644 --- a/.src/state.cpp +++ b/.src/state.cpp @@ -285,7 +285,7 @@ namespace jluna::State::detail UndefInitializer_t = Type(unroll("Core.UndefInitializer")); Union_t = Type(unroll("Core.Union")); UnionAll_t = Type(unroll("Core.UnionAll")); - UnionEmpty_t = Type((jl_datatype_t*) jl_eval_string("return Union{}")); + //UnionEmpty_t = Type((jl_datatype_t*) jl_eval_string("return Union{}")); Unsigned_t = Type(unroll("Core.Unsigned")); VecElement_t = Type(unroll("Core.VecElement")); WeakRef_t = Type(unroll("Core.WeakRef")); diff --git a/.src/usertype.inl b/.src/usertype.inl index c429eec..ea88d7b 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -20,29 +20,18 @@ namespace jluna str << "\tb) UserType::set_boxing_routine was used to implement the boxing routine" << std::endl; str << "\tc) UserType::set_unboxing_routine was used to implement the unboxing routine" << std::endl; str << "\td) after all of the above, UserType::implement was called exactly once" << std::endl; - return str.str.c_str(); + return str.str().c_str(); } template void UserType::pre_initialize() { - if (_already_pre_initialized) + if (_pre_initialized) return; throw_if_uninitialized(); jl_gc_pause; _template = Proxy(jl_eval_string("return jluna.usertype.new_usertype(:unitialized)")); - - _boxing_routine = [](T _) -> Any* { - throw UserTypeNotFullyInitializedException(); - return jl_nothing; - }; - - _unboxing_routine = [](Any* _) -> T { - throw UserTypeNotFullyInitializedException(); - return; - }; - jl_gc_unpause; } @@ -50,7 +39,9 @@ namespace jluna void UserType::set_name(const std::string& name) { pre_initialize(); - _template["_name"] = jl_symbol(name.c_str()); + jl_gc_pause; + _template["_name"] = Symbol(name); + jl_gc_unpause; _name_set = true; } @@ -72,7 +63,7 @@ namespace jluna bool UserType::is_mutable() { pre_initialize(); - return _template["_is_mutable"] = b; + return _template["_is_mutable"]; } template @@ -101,7 +92,7 @@ namespace jluna { pre_initialize(); - _boxing_routine = lamdba; + _boxing_routine = lambda; _boxing_routine_set = true; } @@ -115,7 +106,7 @@ namespace jluna } template - void UserType::implement(Module module) + Type UserType::implement(Module module) { pre_initialize(); @@ -127,7 +118,7 @@ namespace jluna jl_gc_pause; static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); - jluna::safe_call(implement, _template, module); + auto res = Type((jl_datatype_t*) jluna::safe_call(implement, _template, module)); _implemented = true; jl_gc_unpause; } @@ -145,7 +136,7 @@ namespace jluna } template - static Proxy UserType::create_instance(T in) + Proxy UserType::create_instance(T in) { jl_gc_pause; auto out = Proxy(box(in)); diff --git a/.test/main.cpp b/.test/main.cpp index 145a3db..5306d07 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -17,21 +17,21 @@ struct NonJuliaType : _member_var(in) {} - T _member_var; + T member_function() {return _member_var * _member_var;} - int member_function() - { - return _member_var * _member_var; - } + T get_member() const {return _member_var;} + + void set_member(T val) {_member_var = val;} + + private: + T _member_var; }; int main() { State::initialize(); - auto ut = UserType>("NonJuliaType", [](){ - - }); + UserType>::set_name("NonJuliaType"); return 0; diff --git a/CMakeLists.txt b/CMakeLists.txt index 242c5ac..e086427 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16) project(jluna) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_BUILD_TYPE Release) +set(CMAKE_BUILD_TYPE Debug) # compiler support if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "12.0.0") diff --git a/include/jluna.jl b/include/jluna.jl index e3c8510..70906b3 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -656,7 +656,13 @@ module jluna make_named_proxy_id(id::Number, owner_id::ProxyID) ::ProxyID = return Expr(:ref, owner_id, convert(Int64, id)) # assign to proxy id - assign(new_value::T, name::ProxyID) where T = return Main.eval(Expr(:(=), name, new_value)); + function assign(new_value::T, name::ProxyID) where T + if new_value isa Symbol || new_value isa Expr + return Main.eval(Expr(:(=), name, QuoteNode(new_value))) + else + return Main.eval(Expr(:(=), name, new_value)); + end + end # eval proxy id evaluate(name::ProxyID) ::Any = return Main.eval(name) diff --git a/include/usertype.hpp b/include/usertype.hpp index a2e1301..0e1abd4 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -99,9 +99,8 @@ namespace jluna static std::function _unboxing_routine; static inline bool _unboxing_routine_set = false; - bool _name_set = false; - bool _implemented = false; - + static inline bool _name_set = false; + static inline bool _implemented = false; }; /// @brief unbox using unboxing routine From 07a238923639892b049c84766e39bfd84a56bd61 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 19:50:57 +0100 Subject: [PATCH 14/56] stashing before rework --- .test/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.test/main.cpp b/.test/main.cpp index 5306d07..94b6446 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -32,7 +32,7 @@ int main() State::initialize(); UserType>::set_name("NonJuliaType"); - + UserType>::add_field("_member_var", Int64(0)); return 0; From fc20112508963d68a8fa2fd29d3ef12612ac1783 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 21:04:27 +0100 Subject: [PATCH 15/56] stashing --- .src/usertype.inl | 71 ++++++++++++++++++++++++-------------------- .test/main.cpp | 14 ++++++++- include/jluna.jl | 17 ++++------- include/usertype.hpp | 48 ++++++------------------------ 4 files changed, 65 insertions(+), 85 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index ea88d7b..ca22991 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -33,6 +33,8 @@ namespace jluna jl_gc_pause; _template = Proxy(jl_eval_string("return jluna.usertype.new_usertype(:unitialized)")); jl_gc_unpause; + + _pre_initialized = true; } template @@ -40,7 +42,8 @@ namespace jluna { pre_initialize(); jl_gc_pause; - _template["_name"] = Symbol(name); + static jl_function_t* setfield = jl_get_function(jl_base_module, "setfield!"); + jluna::safe_call(setfield, _template, jl_symbol("_name"), jl_symbol(name.c_str())); jl_gc_unpause; _name_set = true; } @@ -67,60 +70,71 @@ namespace jluna } template - template - void UserType::add_field(const std::string& name, Value_t initial_value) + void UserType::add_field(const std::string& name, std::function box_get, std::function unbox_set) { pre_initialize(); - jl_gc_pause; - static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); - jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), box(initial_value)); - jl_gc_unpause; + _mapping.insert({name, {box_get, unbox_set}}); } template - void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) + Any* UserType::box(T in) { - pre_initialize(); + if (not _implemented) + throw UserTypeNotFullyInitializedException(); + jl_gc_pause; - static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); - jluna::safe_call(add_parameter,_template, jl_symbol(name.c_str()), upper_bound, lower_bound); + static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + + for (auto pair : _mapping) + jluna::safe_call(add_field, _template, jl_symbol(pair.first.c_str()), pair.second.first(in)); + + auto* out = jluna::safe_call(_implemented_type, _template); jl_gc_unpause; + return out; } template - void UserType::set_boxing_routine(std::function lambda) + T UserType::unbox(Any* in) { - pre_initialize(); + if (not _implemented) + throw UserTypeNotFullyInitializedException(); + + jl_gc_pause; + static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); - _boxing_routine = lambda; - _boxing_routine_set = true; + auto out = T(); //todo enforce this by concept + + for (auto pair : _mapping) + pair.second.second(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); + + return out; } template - void UserType::set_unboxing_routine(std::function lambda) + void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) { pre_initialize(); - - _unboxing_routine = lambda; - _unboxing_routine_set = true; + jl_gc_pause; + static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); + jluna::safe_call(add_parameter,_template, jl_symbol(name.c_str()), upper_bound, lower_bound); + jl_gc_unpause; } template Type UserType::implement(Module module) { pre_initialize(); - - if (_implemented) - throw UserTypeAlreadyImplementedException(); if (not is_initialized()) throw UserTypeNotFullyInitializedException(); jl_gc_pause; static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); - auto res = Type((jl_datatype_t*) jluna::safe_call(implement, _template, module)); + _implemented_type = jluna::safe_call(implement, _template, module); _implemented = true; jl_gc_unpause; + + return Type((jl_datatype_t*) _implemented_type); } template @@ -132,15 +146,6 @@ namespace jluna template bool UserType::is_initialized() { - return _boxing_routine_set && _unboxing_routine_set && _name_set; - } - - template - Proxy UserType::create_instance(T in) - { - jl_gc_pause; - auto out = Proxy(box(in)); - jl_gc_unpause; - return out; + return _name_set; } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 94b6446..75eedd1 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -13,6 +13,8 @@ using namespace jluna::detail; template struct NonJuliaType { + NonJuliaType() = default; + inline NonJuliaType(T in) : _member_var(in) {} @@ -32,8 +34,18 @@ int main() State::initialize(); UserType>::set_name("NonJuliaType"); - UserType>::add_field("_member_var", Int64(0)); + UserType>::add_field( + "_member_var", + [](NonJuliaType& in) -> Any* {return box(in.get_member());}, + [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} + ); + + UserType>::implement(); + + auto mine = NonJuliaType(1234); + auto* out = UserType>::box(mine); + Base["println"](out); return 0; Test::initialize(); diff --git a/include/jluna.jl b/include/jluna.jl index 70906b3..7991cb5 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1030,7 +1030,7 @@ module jluna evaluate the type """ - function implement(type::UserType, m::Module = Main) ::Type + function implement(type::UserType, m::Module = Main) parameters = Expr(:curly, type._name) for tv in type._parameters @@ -1087,8 +1087,9 @@ module jluna out::Expr = :(mutable struct $(parameters) end) out.args[3] = block - Base.eval(m, out); - return Base.eval(m, type._name) + println(out) + Base.eval(m, out) + return getfield(m, type._name) end export implement end @@ -1124,12 +1125,4 @@ function cppcall(function_name::Symbol, xs...) ::Any return jluna._cppcall.get_result() end -export cppcall - -instance = jluna.usertype.UserType(:TestType) -jluna.usertype.add_parameter!(instance, :T, Integer) -jluna.usertype.set_mutable!(instance, true) -jluna.usertype.add_field!(instance, :_field01, 1234) -jluna.usertype.add_field!(instance, :_field02, 4567) -jluna.usertype.add_field!(instance, :f, x -> println(x)) -jluna.usertype.implement(instance) \ No newline at end of file +export cppcall \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 0e1abd4..83aee83 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -12,9 +12,6 @@ namespace jluna { - template - struct UserTypeAlreadyImplementedException; - template struct UserTypeNotFullyInitializedException; @@ -47,8 +44,7 @@ namespace jluna /// @param name of field /// @param type of field /// @param initial value - template - static void add_field(const std::string& name, Value_t initial_value); + static void add_field(const std::string& name, std::function box_get, std::function unbox_set); /// @brief add parameter /// @param name: e.g. T @@ -56,16 +52,6 @@ namespace jluna /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: T static void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); - /// @brief boxing routine called during box> - /// @param: lambda - /// @note c.f. https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes - static void set_boxing_routine(std::function lambda); - - /// @brief boxing routine called during unbox> - /// @param: lambda - /// @note c.f. https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes - static void set_unboxing_routine(std::function lambda); - /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in /// @returns julia-side type @@ -82,10 +68,8 @@ namespace jluna /// @brief no ctor UserType() = delete; - /// @brief instance this type using an original - /// @param cpp_side_type - /// @returns proxy to new instance - static Proxy create_instance(T); + static Any* box(T); + static T unbox(Any*); private: static void pre_initialize(); @@ -93,14 +77,11 @@ namespace jluna static inline bool _pre_initialized = false; static inline Proxy _template = Proxy(jl_nothing); - static std::function _boxing_routine; - static inline bool _boxing_routine_set = false; - - static std::function _unboxing_routine; - static inline bool _unboxing_routine_set = false; - static inline bool _name_set = false; static inline bool _implemented = false; + + static inline std::map, std::function>> _mapping = {}; + static inline Any* _implemented_type = nullptr; }; /// @brief unbox using unboxing routine @@ -109,13 +90,7 @@ namespace jluna template>, Bool> = true> - inline T unbox(Any* in) - { - if(not T::is_implemented() or not T::is_initialized()) - throw UserTypeNotFullyInitializedException(); - - return T::unboxing_routine(in); - } + T unbox(Any* in); /// @brief box using boxing routine /// @param @@ -123,14 +98,9 @@ namespace jluna template>, Bool> = true> - inline Any* box(T in) - { - if(not T::is_implemented() or not T::is_initialized()) - throw UserTypeNotFullyInitializedException(); - - return T::boxing_routine(in); - } + Any* box(T in); + /// @brief exception thrown template struct UserTypeNotFullyInitializedException : public std::exception { From e79d93e124a8a6509e52ee1d83827fbd3d835c12 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 23:15:11 +0100 Subject: [PATCH 16/56] stashing --- .src/usertype.inl | 31 ++++++++++++++++++++------ .test/main.cpp | 53 ++++++++++++++++++++++++++------------------ include/jluna.jl | 18 +++++++++------ include/usertype.hpp | 9 ++++++-- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index ca22991..6e4e85f 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -70,10 +70,10 @@ namespace jluna } template - void UserType::add_field(const std::string& name, std::function box_get, std::function unbox_set) + void UserType::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) { pre_initialize(); - _mapping.insert({name, {box_get, unbox_set}}); + _mapping.insert({name, {box_get, unbox_set, type}}); } template @@ -85,10 +85,17 @@ namespace jluna jl_gc_pause; static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); - for (auto pair : _mapping) - jluna::safe_call(add_field, _template, jl_symbol(pair.first.c_str()), pair.second.first(in)); - - auto* out = jluna::safe_call(_implemented_type, _template); + for (auto& pair : _mapping) + jluna::safe_call( + add_field, + _template, + jl_symbol(pair.first.c_str()), + jl_symbol(std::get<2>(pair.second).c_str()), + std::get<0>(pair.second)(in) + ); + + auto* out = jl_call1(_implemented_type, _template); + forward_last_exception(); jl_gc_unpause; return out; } @@ -105,7 +112,7 @@ namespace jluna auto out = T(); //todo enforce this by concept for (auto pair : _mapping) - pair.second.second(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); + std::get<1>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); return out; } @@ -129,6 +136,16 @@ namespace jluna throw UserTypeNotFullyInitializedException(); jl_gc_pause; + static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + + for (auto& pair : _mapping) + jluna::safe_call( + add_field, + _template, + jl_symbol(pair.first.c_str()), + jl_symbol(std::get<2>(pair.second).c_str()) + ); + static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); _implemented_type = jluna::safe_call(implement, _template, module); _implemented = true; diff --git a/.test/main.cpp b/.test/main.cpp index 75eedd1..8b32583 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -10,44 +10,55 @@ using namespace jluna; using namespace jluna::detail; -template struct NonJuliaType { - NonJuliaType() = default; - - inline NonJuliaType(T in) - : _member_var(in) - {} - - T member_function() {return _member_var * _member_var;} - - T get_member() const {return _member_var;} - - void set_member(T val) {_member_var = val;} + public: + NonJuliaType() = default; + + inline NonJuliaType(Int64 in) + : _member_var(in) + {} + + inline Int64 get_member() const { + return _member_var; + } + + inline void set_member(Int64 val) { + _member_var = val; + } private: - T _member_var; + Int64 _member_var; }; int main() { State::initialize(); - UserType>::set_name("NonJuliaType"); - UserType>::add_field( + UserType::set_name("NonJuliaType"); + UserType::add_field( "_member_var", - [](NonJuliaType& in) -> Any* {return box(in.get_member());}, - [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} + Symbol("Int64"), + [](NonJuliaType& in) -> Any* {return box(in.get_member());}, + [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} ); - UserType>::implement(); + UserType::implement(); + auto cpp_side_instance = NonJuliaType(1234); + + auto _ = State::new_named_undef("julia_side_instance"); + Main["julia_side_instance"] = UserType::box(cpp_side_instance); + + State::eval(R"( + println(typeof(julia_side_instance)) + println(fieldnames(julia_side_instance)) + println(julia_side_instance._member_var) + )"); - auto mine = NonJuliaType(1234); - auto* out = UserType>::box(mine); - Base["println"](out); return 0; + Test::initialize(); Test::test("catch c exception", [](){ diff --git a/include/jluna.jl b/include/jluna.jl index 7991cb5..953ece2 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -970,12 +970,14 @@ module jluna _name::Symbol _is_mutable::Bool - _fields::Dict{Symbol, Any} + _field_types::Dict{Symbol, Symbol} + _field_values::Dict{Symbol, Any} _parameters::Vector{TypeVar} UserType(name::Symbol, is_mutable::Bool = true) = new( name, is_mutable, + Dict{Symbol, Type}(), Dict{Symbol, Any}(), Vector{TypeVar}() ) @@ -1008,8 +1010,10 @@ module jluna add field to usertype, can also be a function """ - function add_field!(x::UserType, name::Symbol, value) ::Nothing - x._fields[name] = value + function add_field!(x::UserType, name::Symbol, type::Symbol, value = missing) ::Nothing + + x._field_types[name] = type + x._field_values[name] = value return nothing end export add_field! @@ -1047,8 +1051,8 @@ module jluna block = Expr(:block) - for (field_name, field_value) in type._fields - push!(block.args, Expr(:(::), field_name, (field_value isa Function ? :(Function) : Symbol(typeof(field_value))))) + for (field_name, field_type) in type._field_types + push!(block.args, Expr(:(::), field_name, (field_type isa Function ? :(Function) : Symbol(field_type)))) end ctor::Expr = :() @@ -1078,9 +1082,9 @@ module jluna ctor = Expr(:(=), where_call, Expr(:call, curly_new)) end - for (field_name, _) in type._fields + for (field_name, field_value) in type._field_values field_symbol = QuoteNode(field_name) - push!(ctor.args[2].args, :(base._fields[$(field_symbol)])) + push!(ctor.args[2].args, :(base._field_values[$(field_symbol)])) end push!(block.args, ctor) diff --git a/include/usertype.hpp b/include/usertype.hpp index 83aee83..cdcb1ce 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -44,7 +44,7 @@ namespace jluna /// @param name of field /// @param type of field /// @param initial value - static void add_field(const std::string& name, std::function box_get, std::function unbox_set); + static void add_field(const std::string& name, const std::string& type_name, std::function box_get, std::function unbox_set); /// @brief add parameter /// @param name: e.g. T @@ -80,7 +80,12 @@ namespace jluna static inline bool _name_set = false; static inline bool _implemented = false; - static inline std::map, std::function>> _mapping = {}; + static inline std::map, // getter + std::function, // setter + const std::string& // symbol of typename + >> _mapping = {}; + static inline Any* _implemented_type = nullptr; }; From a6775dbb4e13dfcdedb246877f88e2d1a58a6563 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 26 Feb 2022 23:20:00 +0100 Subject: [PATCH 17/56] morgen --- .src/usertype.inl | 19 +++++++++++++------ include/usertype.hpp | 3 ++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 6e4e85f..d3f6e2a 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -73,7 +73,14 @@ namespace jluna void UserType::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) { pre_initialize(); - _mapping.insert({name, {box_get, unbox_set, type}}); + _mapping.insert({name, {box_get, unbox_set, Symbol(type)}}); + } + + template + void UserType::add_field(const std::string& name, Type type, std::function box_get, std::function unbox_set) + { + pre_initialize(); + _mapping.insert({name, {box_get, unbox_set, type.get_symbol()}}); } template @@ -89,9 +96,9 @@ namespace jluna jluna::safe_call( add_field, _template, - jl_symbol(pair.first.c_str()), - jl_symbol(std::get<2>(pair.second).c_str()), - std::get<0>(pair.second)(in) + jl_symbol(pair.first.c_str()), // name + jl_symbol(std::get<2>(pair.second).c_str()), // typename + std::get<0>(pair.second)(in) // getted value ); auto* out = jl_call1(_implemented_type, _template); @@ -142,8 +149,8 @@ namespace jluna jluna::safe_call( add_field, _template, - jl_symbol(pair.first.c_str()), - jl_symbol(std::get<2>(pair.second).c_str()) + jl_symbol(pair.first.c_str()), // name + jl_symbol(std::get<2>(pair.second).c_str()) // typename ); static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); diff --git a/include/usertype.hpp b/include/usertype.hpp index cdcb1ce..af55de4 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -45,6 +45,7 @@ namespace jluna /// @param type of field /// @param initial value static void add_field(const std::string& name, const std::string& type_name, std::function box_get, std::function unbox_set); + static void add_field(const std::string& name, Type type, std::function box_get, std::function unbox_set); /// @brief add parameter /// @param name: e.g. T @@ -83,7 +84,7 @@ namespace jluna static inline std::map, // getter std::function, // setter - const std::string& // symbol of typename + Symbol // symbol of typename >> _mapping = {}; static inline Any* _implemented_type = nullptr; From 89c849640e643dd7735fd21acbb8d7326d5a362b Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 27 Feb 2022 18:03:47 +0100 Subject: [PATCH 18/56] working but weird non-static syntax --- .src/usertype.inl | 92 ++++++++------------------------------------ .test/main.cpp | 17 ++++---- include/jluna.jl | 40 +++++++++++++------ include/usertype.hpp | 71 +++++++++++++--------------------- 4 files changed, 80 insertions(+), 140 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index d3f6e2a..1c475b9 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -8,97 +8,62 @@ namespace jluna { template - UserTypeNotFullyInitializedException::UserTypeNotFullyInitializedException() - {} - - template - const char * UserTypeNotFullyInitializedException::what() const noexcept - { - std::stringstream str; - str << "[C++][EXCEPTION] UserType interface for this type has not yet been fully specified, make sure that the following actions were performed:" << std::endl; - str << "\ta) UserType::set_name was used to specify the name" << std::endl; - str << "\tb) UserType::set_boxing_routine was used to implement the boxing routine" << std::endl; - str << "\tc) UserType::set_unboxing_routine was used to implement the unboxing routine" << std::endl; - str << "\td) after all of the above, UserType::implement was called exactly once" << std::endl; - return str.str().c_str(); - } - - template - void UserType::pre_initialize() + UserType::UserType(const std::string& name) { - if (_pre_initialized) - return; - - throw_if_uninitialized(); jl_gc_pause; - _template = Proxy(jl_eval_string("return jluna.usertype.new_usertype(:unitialized)")); + static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); + _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str()))); jl_gc_unpause; - - _pre_initialized = true; + set_name(name); } template void UserType::set_name(const std::string& name) { - pre_initialize(); - jl_gc_pause; - static jl_function_t* setfield = jl_get_function(jl_base_module, "setfield!"); - jluna::safe_call(setfield, _template, jl_symbol("_name"), jl_symbol(name.c_str())); - jl_gc_unpause; - _name_set = true; + _template["_name"] = Symbol(name); } template std::string UserType::get_name() { - pre_initialize(); return _template["_name"].operator std::string(); } template void UserType::set_mutable(bool b) { - pre_initialize(); _template["_is_mutable"] = b; } template bool UserType::is_mutable() { - pre_initialize(); return _template["_is_mutable"]; } template void UserType::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) { - pre_initialize(); - _mapping.insert({name, {box_get, unbox_set, Symbol(type)}}); - } + jl_gc_pause; - template - void UserType::add_field(const std::string& name, Type type, std::function box_get, std::function unbox_set) - { - pre_initialize(); - _mapping.insert({name, {box_get, unbox_set, type.get_symbol()}}); + static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + auto res = (*(_mapping.insert({name, {type, box_get, unbox_set}})).first); + jluna::safe_call(add_field, _template, jl_symbol(res.first.c_str()), jl_symbol(std::get<0>(res.second).c_str())); + jl_gc_unpause; } template Any* UserType::box(T in) { - if (not _implemented) - throw UserTypeNotFullyInitializedException(); - jl_gc_pause; - static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); + static jl_function_t* set_field = jl_find_function("jluna.usertype", "set_field!"); for (auto& pair : _mapping) jluna::safe_call( - add_field, + set_field, _template, - jl_symbol(pair.first.c_str()), // name - jl_symbol(std::get<2>(pair.second).c_str()), // typename - std::get<0>(pair.second)(in) // getted value + jl_symbol(pair.first.c_str()), + std::get<1>(pair.second)(in) ); auto* out = jl_call1(_implemented_type, _template); @@ -110,24 +75,21 @@ namespace jluna template T UserType::unbox(Any* in) { - if (not _implemented) - throw UserTypeNotFullyInitializedException(); - jl_gc_pause; static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); - auto out = T(); //todo enforce this by concept + auto out = T(); for (auto pair : _mapping) - std::get<1>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); + std::get<2>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); + jl_gc_unpause; return out; } template void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) { - pre_initialize(); jl_gc_pause; static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); jluna::safe_call(add_parameter,_template, jl_symbol(name.c_str()), upper_bound, lower_bound); @@ -137,21 +99,7 @@ namespace jluna template Type UserType::implement(Module module) { - pre_initialize(); - - if (not is_initialized()) - throw UserTypeNotFullyInitializedException(); - jl_gc_pause; - static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); - - for (auto& pair : _mapping) - jluna::safe_call( - add_field, - _template, - jl_symbol(pair.first.c_str()), // name - jl_symbol(std::get<2>(pair.second).c_str()) // typename - ); static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); _implemented_type = jluna::safe_call(implement, _template, module); @@ -166,10 +114,4 @@ namespace jluna { return _implemented; } - - template - bool UserType::is_initialized() - { - return _name_set; - } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 8b32583..1c3d7c5 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -35,32 +35,31 @@ int main() { State::initialize(); - UserType::set_name("NonJuliaType"); - UserType::add_field( + auto usertype = UserType("NonJuliaType"); + usertype.set_name("NonJuliaType"); + usertype.add_field( "_member_var", - Symbol("Int64"), + "Int64", [](NonJuliaType& in) -> Any* {return box(in.get_member());}, [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} ); - UserType::implement(); + usertype.implement(); auto cpp_side_instance = NonJuliaType(1234); auto _ = State::new_named_undef("julia_side_instance"); - Main["julia_side_instance"] = UserType::box(cpp_side_instance); + Main["julia_side_instance"] = usertype.box(cpp_side_instance); - State::eval(R"( + jl_eval_string(R"( println(typeof(julia_side_instance)) - println(fieldnames(julia_side_instance)) + println(fieldnames(typeof(julia_side_instance))) println(julia_side_instance._member_var) )"); return 0; - Test::initialize(); - Test::test("catch c exception", [](){ Test::assert_that_throws([](){ diff --git a/include/jluna.jl b/include/jluna.jl index 953ece2..b376838 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1006,18 +1006,30 @@ module jluna export set_mutable! """ - `add_field!(::UserType, ::Symbol, value) -> Nothing` + `add_field!(::UserType, name::Symbol, typename::Symbol) -> Nothing` add field to usertype, can also be a function """ - function add_field!(x::UserType, name::Symbol, type::Symbol, value = missing) ::Nothing + function add_field!(x::UserType, name::Symbol, type::Symbol) ::Nothing x._field_types[name] = type - x._field_values[name] = value + x._field_values[name] = missing return nothing end export add_field! + """ + `set_field!(::UserType, ::Symbol, value) -> Nothing` + + set value of field in usertype + """ + function set_field!(x::UserType, name::Symbol, value) ::Nothing + + @assert haskey(x._field_values, name) + x._field_values[name] = value + return nothing; + end + """ `add_parameter!(::UserType, ::Symbol, upper_bound::Type, lower_bound::Type) -> Nothing` @@ -1036,16 +1048,22 @@ module jluna """ function implement(type::UserType, m::Module = Main) - parameters = Expr(:curly, type._name) - for tv in type._parameters - if tv.lb == Union{} - if tv.ub == Any - push!(param.args, tv.name) + parameters = :() + + if isempty(type._parameters) + parameters = type._name + else + parameters = Expr(:curly, type._name) + for tv in type._parameters + if tv.lb == Union{} + if tv.ub == Any + push!(param.args, tv.name) + else + push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) + end else - push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) + push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) end - else - push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) end end diff --git a/include/usertype.hpp b/include/usertype.hpp index af55de4..2e80659 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -12,82 +12,76 @@ namespace jluna { - template - struct UserTypeNotFullyInitializedException; - /// @brief customizable wrapper for non-julia type T /// @note for information on how to use this class, visit https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes template class UserType { + static inline std::function noop_set = [](T&, Any* ) {return;}; + public: /// @brief original type using original_type = T; + /// + UserType(const std::string& name); + /// @brief set julia-side name /// @param name - static void set_name(const std::string& name); + void set_name(const std::string& name); /// @brief get julia-side name /// @returns name - static std::string get_name(); + std::string get_name(); /// @brief set mutability, no by default /// @param bool - static void set_mutable(bool); + void set_mutable(bool); /// @brief get mutability /// @returns bool - static bool is_mutable(); + bool is_mutable(); /// @brief add field - /// @param name of field - /// @param type of field - /// @param initial value - static void add_field(const std::string& name, const std::string& type_name, std::function box_get, std::function unbox_set); - static void add_field(const std::string& name, Type type, std::function box_get, std::function unbox_set); + void add_field( + const std::string& name, + const std::string& type_name, + std::function box_get, + std::function unbox_set = noop_set + ); /// @brief add parameter /// @param name: e.g. T /// @param upper_bound: .ub of TypeVar, equivalent to T <: upper_bound /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: T - static void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in /// @returns julia-side type - static Type implement(Module module = Main); + Type implement(Module module = Main); /// @brief is already implemented /// @brief true if implement was called, false otherwise - static bool is_implemented(); - - /// @brief is fully initialized - /// @returns true if boxing, unboxing and name was set, false otherwise - static bool is_initialized(); + bool is_implemented(); /// @brief no ctor UserType() = delete; - static Any* box(T); - static T unbox(Any*); + Any* box(T); + T unbox(Any*); private: - static void pre_initialize(); - - static inline bool _pre_initialized = false; - static inline Proxy _template = Proxy(jl_nothing); + Proxy _template; - static inline bool _name_set = false; - static inline bool _implemented = false; + bool _implemented = false; + Any* _implemented_type = nullptr; - static inline std::map, // getter - std::function, // setter - Symbol // symbol of typename + std::function // setter >> _mapping = {}; - - static inline Any* _implemented_type = nullptr; }; /// @brief unbox using unboxing routine @@ -105,19 +99,6 @@ namespace jluna typename U = typename T::original_type, typename std::enable_if_t>, Bool> = true> Any* box(T in); - - /// @brief exception thrown - template - struct UserTypeNotFullyInitializedException : public std::exception - { - /// @brief ctor - /// @param name - UserTypeNotFullyInitializedException(); - - /// @brief what - /// @returns message - virtual const char* what() const noexcept override final; - }; } #include ".src/usertype.inl" \ No newline at end of file From 97d2c512af46c6e1fdd9acbbe594021b87b73961 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 27 Feb 2022 18:30:02 +0100 Subject: [PATCH 19/56] stashing --- .src/usertype.inl | 13 +++++++++++++ include/usertype.hpp | 36 +++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 1c475b9..94f4eb2 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -114,4 +114,17 @@ namespace jluna { return _implemented; } + + template>, Bool>> + T unbox(Any* in) + { + return UserType::unbox(in); + } + + template>, Bool>> + Any* box(T in) + { + return UserType::box(in); + } + } \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 2e80659..c55cde1 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -23,27 +23,27 @@ namespace jluna /// @brief original type using original_type = T; - /// + /// @brief instance UserType(const std::string& name); /// @brief set julia-side name /// @param name - void set_name(const std::string& name); + static void set_name(const std::string& name); /// @brief get julia-side name /// @returns name - std::string get_name(); + static std::string get_name(); /// @brief set mutability, no by default /// @param bool - void set_mutable(bool); + static void set_mutable(bool); /// @brief get mutability /// @returns bool - bool is_mutable(); + static bool is_mutable(); /// @brief add field - void add_field( + static void add_field( const std::string& name, const std::string& type_name, std::function box_get, @@ -54,30 +54,30 @@ namespace jluna /// @param name: e.g. T /// @param upper_bound: .ub of TypeVar, equivalent to T <: upper_bound /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: T - void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + static void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in /// @returns julia-side type - Type implement(Module module = Main); + static Type implement(Module module = Main); /// @brief is already implemented /// @brief true if implement was called, false otherwise - bool is_implemented(); + static bool is_implemented(); - /// @brief no ctor - UserType() = delete; + /// @brief box interface + static Any* box(T); - Any* box(T); - T unbox(Any*); + /// @brief unbox interface + static T unbox(Any*); private: - Proxy _template; + static inline Proxy _template = Proxy(jl_nothing); - bool _implemented = false; - Any* _implemented_type = nullptr; + static inline bool _implemented = false; + static inline Any* _implemented_type = nullptr; - std::map, // getter std::function // setter @@ -99,6 +99,8 @@ namespace jluna typename U = typename T::original_type, typename std::enable_if_t>, Bool> = true> Any* box(T in); + + } #include ".src/usertype.inl" \ No newline at end of file From 8aec1bddb033f887c79001a37e9da23a3b0a8c2f Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 27 Feb 2022 18:41:52 +0100 Subject: [PATCH 20/56] stashing --- .src/usertype.inl | 28 ++++++++++++++++++++++++---- .test/main.cpp | 4 ++-- include/box.hpp | 9 +++++++++ include/unbox.hpp | 9 +++++++++ include/usertype.hpp | 27 +++++++++++---------------- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 94f4eb2..ae275f0 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -7,6 +7,20 @@ namespace jluna { + template + UserTypeNotFullyInitializedException::UserTypeNotFullyInitializedException() + {} + + template + const char * UserTypeNotFullyInitializedException::what() const noexcept + { + std::stringstream str; + str << "[C++][EXCEPTION] UserType interface for this type has not yet been fully specified, make sure that the following actions were performed:" << std::endl; + str << "\ta) UserType::UserType(const std::string&) was used instance the type at least once" << std::endl; + str << "\tb) After calling the ctor, UserType::implement was called exactly once" << std::endl; + return str.str().c_str(); + } + template UserType::UserType(const std::string& name) { @@ -116,15 +130,21 @@ namespace jluna } template>, Bool>> - T unbox(Any* in) + U unbox(Any* in) { - return UserType::unbox(in); + if (not UserType::is_implemented()) + throw UserTypeNotFullyInitializedException(); + + return UserType::unbox(in); } template>, Bool>> - Any* box(T in) + Any* box(U in) { - return UserType::box(in); + if (not UserType::is_implemented()) + throw UserTypeNotFullyInitializedException(); + + return UserType::box(in); } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 1c3d7c5..99064ad 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -44,12 +44,12 @@ int main() [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} ); - usertype.implement(); + //usertype.implement(); auto cpp_side_instance = NonJuliaType(1234); auto _ = State::new_named_undef("julia_side_instance"); - Main["julia_side_instance"] = usertype.box(cpp_side_instance); + Main["julia_side_instance"] = box>(cpp_side_instance); jl_eval_string(R"( println(typeof(julia_side_instance)) diff --git a/include/box.hpp b/include/box.hpp index 8b3cf4b..66a2b44 100644 --- a/include/box.hpp +++ b/include/box.hpp @@ -185,6 +185,15 @@ namespace jluna template T> Any* box(T); + /// @brief box usertype to usertype + template + class UserType; + + template>, Bool> = true> + Any* box(U in); + /// @concept requires a value to be boxable into a julia-side value template concept Boxable = requires(T t) diff --git a/include/unbox.hpp b/include/unbox.hpp index 6984548..cf6818b 100644 --- a/include/unbox.hpp +++ b/include/unbox.hpp @@ -145,6 +145,15 @@ namespace jluna template T> T unbox(Any* value); + /// @brief unbox usertype to usertype + template + class UserType; + + template>, Bool> = true> + U unbox(Any* in); + /// @concept requires a value to be unboxed from a julia-side value template concept Unboxable = requires(T t, jl_value_t* v) diff --git a/include/usertype.hpp b/include/usertype.hpp index c55cde1..4d4b9b4 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -84,23 +84,18 @@ namespace jluna >> _mapping = {}; }; - /// @brief unbox using unboxing routine - /// @param pointer - /// @returns T - template>, Bool> = true> - T unbox(Any* in); - - /// @brief box using boxing routine - /// @param - /// @returns pointer to julia-side memory - template>, Bool> = true> - Any* box(T in); - + /// @brief exception thrown when usertype is used before being implemented + template + struct UserTypeNotFullyInitializedException : public std::exception + { + /// @brief ctor + /// @param name + UserTypeNotFullyInitializedException(); + /// @brief what + /// @returns message + virtual const char* what() const noexcept override final; + }; } #include ".src/usertype.inl" \ No newline at end of file From 4cf211af02b1b79660a4dff911a71aa13ea83f88 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 27 Feb 2022 19:34:28 +0100 Subject: [PATCH 21/56] working --- .src/usertype.inl | 27 ++++++++++++++++++++------- .test/main.cpp | 38 +++++++++++++++++++++++++++----------- include/box.hpp | 8 +++++++- include/unbox.hpp | 2 +- include/usertype.hpp | 25 +++++++++++++++++++------ 5 files changed, 74 insertions(+), 26 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index ae275f0..6ecb9e2 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -9,16 +9,18 @@ namespace jluna { template UserTypeNotFullyInitializedException::UserTypeNotFullyInitializedException() - {} - - template - const char * UserTypeNotFullyInitializedException::what() const noexcept { std::stringstream str; - str << "[C++][EXCEPTION] UserType interface for this type has not yet been fully specified, make sure that the following actions were performed:" << std::endl; + str << "[C++][EXCEPTION] UserType interface for this type has not yet been implemented, make sure that the following actions were performed:" << std::endl; str << "\ta) UserType::UserType(const std::string&) was used instance the type at least once" << std::endl; str << "\tb) After calling the ctor, UserType::implement was called exactly once" << std::endl; - return str.str().c_str(); + _msg = str.str(); + } + + template + const char * UserTypeNotFullyInitializedException::what() const noexcept + { + return _msg.c_str(); } template @@ -69,6 +71,9 @@ namespace jluna template Any* UserType::box(T in) { + if (not is_implemented()) + throw UserTypeNotFullyInitializedException(); + jl_gc_pause; static jl_function_t* set_field = jl_find_function("jluna.usertype", "set_field!"); @@ -89,13 +94,16 @@ namespace jluna template T UserType::unbox(Any* in) { + if (not is_implemented()) + throw UserTypeNotFullyInitializedException(); + jl_gc_pause; static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); auto out = T(); for (auto pair : _mapping) - std::get<2>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first))); + std::get<2>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first.c_str()))); jl_gc_unpause; return out; @@ -147,4 +155,9 @@ namespace jluna return UserType::box(in); } + template + Any* box(UserTypeWrapper wrapper) + { + UserType::box(wrapper.get()); + } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 99064ad..2215609 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -10,52 +10,68 @@ using namespace jluna; using namespace jluna::detail; +// julia incompatible arbitrary type struct NonJuliaType { public: NonJuliaType() = default; inline NonJuliaType(Int64 in) - : _member_var(in) + : _member(in) {} inline Int64 get_member() const { - return _member_var; + return _member; } inline void set_member(Int64 val) { - _member_var = val; + _member = val; } private: - Int64 _member_var; + Int64 _member; }; int main() { State::initialize(); + // setup usertype interface auto usertype = UserType("NonJuliaType"); - usertype.set_name("NonJuliaType"); usertype.add_field( - "_member_var", - "Int64", + "_member", + "Int64", [](NonJuliaType& in) -> Any* {return box(in.get_member());}, [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} ); - //usertype.implement(); + // implement + usertype.implement(); + // cpp-side instance... auto cpp_side_instance = NonJuliaType(1234); - auto _ = State::new_named_undef("julia_side_instance"); - Main["julia_side_instance"] = box>(cpp_side_instance); + // ... can now be transfered to julia + State::new_named_undef("julia_side_instance") = box>(cpp_side_instance); + // print, then modify instance julia-side + jl_eval_string(R"( + println(julia_side_instance) + julia_side_instance._member = 4567 + )"); + + // move back cpp-side + auto back_cpp_side = unbox>(jl_eval_string("return julia_side_instance")); + std::cout << back_cpp_side.get_member() << std::endl; + + /* + jl_eval_string(R"( println(typeof(julia_side_instance)) println(fieldnames(typeof(julia_side_instance))) - println(julia_side_instance._member_var) + println(julia_side_instance._member) )"); + */ return 0; diff --git a/include/box.hpp b/include/box.hpp index 66a2b44..b413c2d 100644 --- a/include/box.hpp +++ b/include/box.hpp @@ -185,7 +185,7 @@ namespace jluna template T> Any* box(T); - /// @brief box usertype to usertype + /// @brief box usertype wrapper to usertype template class UserType; @@ -194,6 +194,12 @@ namespace jluna typename std::enable_if_t>, Bool> = true> Any* box(U in); + template + class UserTypeWrapper; + + template + Any* box(UserTypeWrapper); + /// @concept requires a value to be boxable into a julia-side value template concept Boxable = requires(T t) diff --git a/include/unbox.hpp b/include/unbox.hpp index cf6818b..cb2d05a 100644 --- a/include/unbox.hpp +++ b/include/unbox.hpp @@ -145,7 +145,7 @@ namespace jluna template T> T unbox(Any* value); - /// @brief unbox usertype to usertype + /// @brief unbox usertype wrapper to usertype template class UserType; diff --git a/include/usertype.hpp b/include/usertype.hpp index 4d4b9b4..1feb881 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -84,17 +84,30 @@ namespace jluna >> _mapping = {}; }; + /// @brief wrapper + template + struct UserTypeWrapper : public std::reference_wrapper + { + UserTypeWrapper(T& in) + : std::reference_wrapper(in) + {} + }; + /// @brief exception thrown when usertype is used before being implemented template struct UserTypeNotFullyInitializedException : public std::exception { - /// @brief ctor - /// @param name - UserTypeNotFullyInitializedException(); + public: + /// @brief ctor + /// @param name + UserTypeNotFullyInitializedException(); - /// @brief what - /// @returns message - virtual const char* what() const noexcept override final; + /// @brief what + /// @returns message + const char* what() const noexcept override final; + + private: + std::string _msg; }; } From 42c9bd402759e2011f8dff256aef8bf697b6b696 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 27 Feb 2022 21:30:25 +0100 Subject: [PATCH 22/56] stashing --- .src/usertype.inl | 11 ++++++ .test/main.cpp | 44 ++---------------------- docs/manual.md | 81 ++++++++++++++++++++++++++++++++++++++++++-- include/concepts.hpp | 7 ++++ 4 files changed, 98 insertions(+), 45 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 6ecb9e2..cf6115a 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -29,6 +29,7 @@ namespace jluna jl_gc_pause; static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str()))); + to_julia_type>::type_name = name; jl_gc_unpause; set_name(name); } @@ -160,4 +161,14 @@ namespace jluna { UserType::box(wrapper.get()); } + + namespace detail + { + template + struct to_julia_type_aux> + { + // usertype name is only available, after UserType::UserType(T) was called at least once + static inline std::string type_name = ""; + }; + } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 2215609..e864104 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -15,18 +15,6 @@ struct NonJuliaType { public: NonJuliaType() = default; - - inline NonJuliaType(Int64 in) - : _member(in) - {} - - inline Int64 get_member() const { - return _member; - } - - inline void set_member(Int64 val) { - _member = val; - } private: Int64 _member; @@ -38,40 +26,12 @@ int main() // setup usertype interface auto usertype = UserType("NonJuliaType"); - usertype.add_field( + /*usertype.add_field( "_member", "Int64", [](NonJuliaType& in) -> Any* {return box(in.get_member());}, [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} - ); - - // implement - usertype.implement(); - - // cpp-side instance... - auto cpp_side_instance = NonJuliaType(1234); - - // ... can now be transfered to julia - State::new_named_undef("julia_side_instance") = box>(cpp_side_instance); - - // print, then modify instance julia-side - jl_eval_string(R"( - println(julia_side_instance) - julia_side_instance._member = 4567 - )"); - - // move back cpp-side - auto back_cpp_side = unbox>(jl_eval_string("return julia_side_instance")); - std::cout << back_cpp_side.get_member() << std::endl; - - /* - - jl_eval_string(R"( - println(typeof(julia_side_instance)) - println(fieldnames(typeof(julia_side_instance))) - println(julia_side_instance._member) - )"); - */ + );*/ return 0; diff --git a/docs/manual.md b/docs/manual.md index 9428a46..2487100 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -270,11 +270,16 @@ std::function => function (::Any, ::Any) -> Any std::function => function (::Any, ::Any, ::Any) -> Any std::function => function (::Any, ::Any, ::Any, ::Any) -> Any std::function)> => function (::Vector{Any}) ::Any + +UserType => T * + +* where T is an arbitrary C++ type ``` -We will learn more on how to box/unbox functions, specifically, in the [section on calling C++ functions from julia](#functions). +We will learn more on how to box/unbox functions, specifically, in the [section on calling C++ functions from julia](#functions). We can (un)box truly arbitrary C++ types through the [usertype interface](#usertypes), which we will explore later on, aswell. -The template meta function `to_julia_type` is provided to convert a C++ type into a julia-type. `to_julia_type::type_name` is the name of the type as a string. + +For any of the above, the template meta function `to_julia_type` is provided to convert a C++ type into a julia-type, where `to_julia_type::type_name` is the name of the type as a string: ```cpp std::cout << to_julia_type>::type_name << std::endl; @@ -1834,7 +1839,77 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes -(this feature is not yet implemented) +So far, we could only exchange information between the Julia and C++ state if its type was (un)boxable. This list of types while broad, is limited. This is why `jluna` offers an interface that allows users to specify their own (un)boxing routines and make *arbitrary* C++ types (un)boxable. We do this through a wrapper type `jluna::UserType`. + +Unlike the previous sections, it may be best to illustrate the use of the usertype interface through an example. Let's say we want the following C++ class to be (un)boxable: + +```cpp +class Quiver +{ + public: + struct Arrow + { + const size_t _unique_id; + }; + + Quiver() + : _holds(); + {} + + void add(Arrow arrow) + { + _holds.push_back(arrow); + } + + Arrow remove() + { + auto out = _holds.front(); + _holds.erase(_holds.begin()); + return out; + } + + size_t size() + { + return _holds.size(); + } + + private: + std::vector _holds; +}; +``` + +Let's first investigate this class using C++ terminology. Firstly, it is call `Quiver` and has an internal, public class called `Arrow`. `Arrow` only has a single, constant member `_unique_id`. `Quiver` has 3 member functions that add an arrow, remove an error and count the number of arrows. The arrows are held in an internal vector. + +When trying to translate this class to julia, we immediately run into a couple of problems. In julia, there are no internal structs in the traditional way. A struct declared inside another struct is available in the same scope. Furthermore, julia does not have traditional member functions, a function that is a member of a struct cannot access any other member in that struct implicitly. We can get around this by employing the following trick: + +```julia +# in cpp + + +Lastly, julia has no concept similar to private or public members, all members are always public. Given this, one way to translate `Quiver` to julia would be the following: + + +```julia +struct Arrow + _unique_id::UInt64 +end + +struct Quiver + + add::Function + remove::Function + size::Function + + _holds::Vector{Arrow} + + Quiver() = new( + function (arrow::Arrow) + + ) +end + + +``` --- diff --git a/include/concepts.hpp b/include/concepts.hpp index 0a11271..ef1684b 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -43,6 +43,13 @@ namespace jluna template concept IsTuple = std::tuple_size::value != 2; + /// @concept: has default ctor + template + concept IsDefaultConstructible = requires(T) + { + {T()}; + }; + /// @concept: is iterable template concept Iterable = requires(T t) From 3ab0e1f3460a3c1a2b8cca4a3e50446ca17c54be Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 18:41:33 +0100 Subject: [PATCH 23/56] stable, debugging istuple --- .src/state.inl | 4 +- .src/unbox.inl | 4 +- .test/main.cpp | 18 ++++--- include/concepts.hpp | 125 ++++++++++++++++++++++++++++++++++--------- include/state.hpp | 4 +- include/unbox.hpp | 5 +- 6 files changed, 117 insertions(+), 43 deletions(-) diff --git a/.src/state.inl b/.src/state.inl index fe7d5cc..2e5b7f8 100644 --- a/.src/state.inl +++ b/.src/state.inl @@ -96,7 +96,7 @@ namespace jluna::State return detail::create_or_assign(name, (jl_value_t*) jl_symbol(value.c_str())); } - template + template Proxy new_named_complex(const std::string& name, T real, T imag) { return detail::create_or_assign(name, box>(std::complex(real, imag))); @@ -236,7 +236,7 @@ namespace jluna::State return Proxy((jl_value_t*) jl_symbol(value.c_str())); } - template + template Proxy new_unnamed_complex(T real, T imag) { return Proxy(box>(std::complex(real, imag))); diff --git a/.src/unbox.inl b/.src/unbox.inl index 189e954..d48ed0d 100644 --- a/.src/unbox.inl +++ b/.src/unbox.inl @@ -184,7 +184,7 @@ namespace jluna return out; } - template>, bool>> + template T unbox(Any* value) { jl_gc_pause; @@ -192,7 +192,7 @@ namespace jluna auto* first = jl_get_nth_field(value, 0); auto* second = jl_get_nth_field(value, 1); - auto res = std::pair(unbox(first), unbox(second)); + auto res = T(unbox(first), unbox(second)); jl_gc_unpause; return res; diff --git a/.test/main.cpp b/.test/main.cpp index e864104..3d9295d 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -6,6 +6,7 @@ #include <.src/c_adapter.hpp> #include #include +#include using namespace jluna; using namespace jluna::detail; @@ -20,21 +21,21 @@ struct NonJuliaType Int64 _member; }; + int main() { State::initialize(); + std::cout << detail::is_tuple_aux, 0, 1, 2> << std::endl; + - // setup usertype interface - auto usertype = UserType("NonJuliaType"); - /*usertype.add_field( - "_member", - "Int64", - [](NonJuliaType& in) -> Any* {return box(in.get_member());}, - [](NonJuliaType& out, Any* field) -> void {out.set_member(unbox(field));} - );*/ + std::cout << IsTuple> << std::endl; + std::cout << IsTuple << std::endl; + std::cout << IsTuple> << std::endl; return 0; + /* + Test::initialize(); Test::test("catch c exception", [](){ @@ -1109,6 +1110,7 @@ int main() }); Test::conclude(); + */ } diff --git a/include/concepts.hpp b/include/concepts.hpp index ef1684b..d2f15fc 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -8,40 +8,18 @@ #include #include #include +#include #include namespace jluna { - /// @concept: can be reinterpret-cast to jl_value_t* - template - concept IsJuliaValuePointer = - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v; - - /// @concept is primitive number type - template - concept IsNumerical = - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v or - std::is_same_v; - /// @concept: wrapper for std::is_same_v template concept Is = std::is_same::value or std::is_same_v; /// @concept: is tuple but not pair - template - concept IsTuple = std::tuple_size::value != 2; + //template + //concept IsTuple = std::tuple_size::value != 2; /// @concept: has default ctor template @@ -64,4 +42,101 @@ namespace jluna /// @concept describes lambda with signature (Args_t...) -> T template concept LambdaType = std::is_invocable::value and not std::is_base_of::value; + + /// @concept: can be reinterpret-cast to jl_value_t* + template + concept IsJuliaValuePointer = + std::is_same_v or + std::is_same_v or + std::is_same_v or + std::is_same_v; + + template + concept IsAnyPtrCastable = requires(T t) + { + static_cast(t); + }; + + /// @concept is primitive + template + concept IsPrimitive = + not Is and + not Is> and + not Is> and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is and + not Is; + + /// @concept is std::complex + template + concept IsComplex = std::is_same_v>; + + /// @concept is std::vector + template + concept IsVector = std::is_same_v>; + + /// @concept is map + template + concept IsMap = + std::is_same_v> or + std::is_same_v> or + std::is_same_v>; + + /// @concept is std::set + template + concept IsSet = std::is_same_v>; + + /// @concept is pair + template + concept IsPair = std::is_same_v> and std::tuple_size_v == 2; + + /// @concept is tuple + namespace detail + { + template + constexpr bool is_tuple_aux(std::index_sequence _) + { + return std::is_same_v...>>; + } + } + + template + concept IsTuple = std::tuple_size::value != 2; + + /* + template + concept IsTuple = requires(T t) + { + {std::tuple_size::value}; + {std::tuple_size_v != 2}; + }; + + /*requires (T t) { + + {std::tuple_size::value != 2}; + //{detail::is_tuple_aux::value>>() and std::tuple_size::value != 2}; + };*/ + + /// @concept not unboxable out-of-the-box + template + concept IsUsertype = + not IsJuliaValuePointer and + not IsAnyPtrCastable and + not IsPrimitive and + not IsComplex and + not IsVector and + not IsMap and + not IsSet and + not IsPair and + not IsTuple; } \ No newline at end of file diff --git a/include/state.hpp b/include/state.hpp index c279343..6670bea 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -161,7 +161,7 @@ namespace jluna::State /// @param real /// @param imaginary /// @returns *named* proxy to newly created value - template + template [[nodiscard]] Proxy new_named_complex(const std::string& name, T real = 0, T imag = 0); /// @brief creates new variable in main, then returns named proxy to it @@ -319,7 +319,7 @@ namespace jluna::State /// @param real /// @param imaginary /// @returns *unnamed* proxy to newly created value - template + template [[nodiscard]] Proxy new_unnamed_complex(T real = 0, T imag = 0); /// @brief creates new variable in main, then returns unnamed proxy to it diff --git a/include/unbox.hpp b/include/unbox.hpp index cb2d05a..30e6329 100644 --- a/include/unbox.hpp +++ b/include/unbox.hpp @@ -115,10 +115,7 @@ namespace jluna T unbox(Any* value); /// @brief unbox to pair - template>, bool> = true> + template T unbox(Any* value); /// @brief unbox to tuple From 07d9d58542b2e17e3f72f497925622cd2360a405 Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 18:53:51 +0100 Subject: [PATCH 24/56] concepts working --- .test/main.cpp | 12 ------------ include/concepts.hpp | 17 ++--------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index 3d9295d..266449e 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -25,17 +25,6 @@ struct NonJuliaType int main() { State::initialize(); - std::cout << detail::is_tuple_aux, 0, 1, 2> << std::endl; - - - - std::cout << IsTuple> << std::endl; - std::cout << IsTuple << std::endl; - std::cout << IsTuple> << std::endl; - return 0; - - /* - Test::initialize(); Test::test("catch c exception", [](){ @@ -1110,7 +1099,6 @@ int main() }); Test::conclude(); - */ } diff --git a/include/concepts.hpp b/include/concepts.hpp index d2f15fc..e292d09 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace jluna { @@ -109,23 +110,9 @@ namespace jluna return std::is_same_v...>>; } } - - template - concept IsTuple = std::tuple_size::value != 2; - - /* template - concept IsTuple = requires(T t) - { - {std::tuple_size::value}; - {std::tuple_size_v != 2}; - }; - - /*requires (T t) { + concept IsTuple = detail::is_tuple_aux(std::make_index_sequence::value>()); - {std::tuple_size::value != 2}; - //{detail::is_tuple_aux::value>>() and std::tuple_size::value != 2}; - };*/ /// @concept not unboxable out-of-the-box template From b00e93c3e6212d35e3b2e4c30d8dd281bffae2ca Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 19:25:49 +0100 Subject: [PATCH 25/56] stashing --- .src/usertype.inl | 68 +++++++++++++++++++---------------------- .test/main.cpp | 32 +++++++++++++++++++- docs/manual.md | 4 +-- include/box.hpp | 15 ++------- include/concepts.hpp | 72 ++++++++++++++++++++++++++++++++++++++------ include/jluna.jl | 38 +++++++++++------------ include/unbox.hpp | 9 ++---- include/usertype.hpp | 17 +++-------- 8 files changed, 153 insertions(+), 102 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index cf6115a..8f5aead 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -8,58 +8,58 @@ namespace jluna { template - UserTypeNotFullyInitializedException::UserTypeNotFullyInitializedException() + UsertypeNotFullyInitializedException::UsertypeNotFullyInitializedException() { std::stringstream str; - str << "[C++][EXCEPTION] UserType interface for this type has not yet been implemented, make sure that the following actions were performed:" << std::endl; - str << "\ta) UserType::UserType(const std::string&) was used instance the type at least once" << std::endl; - str << "\tb) After calling the ctor, UserType::implement was called exactly once" << std::endl; + str << "[C++][EXCEPTION] Usertype interface for this type has not yet been implemented, make sure that the following actions were performed before calling (un)box:" << std::endl; + str << "\ta) Usertype::Usertype(const std::string&) was used instance the usertype interface at least once" << std::endl; + str << "\tb) After calling the ctor, Usertype::implement was called exactly once to push the usertype interface to the julia state" << std::endl; _msg = str.str(); } template - const char * UserTypeNotFullyInitializedException::what() const noexcept + const char * UsertypeNotFullyInitializedException::what() const noexcept { return _msg.c_str(); } template - UserType::UserType(const std::string& name) + void Usertype::enable(const std::string& name) { jl_gc_pause; static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str()))); - to_julia_type>::type_name = name; + detail::to_julia_type_aux>::type_name = name; jl_gc_unpause; set_name(name); } template - void UserType::set_name(const std::string& name) + void Usertype::set_name(const std::string& name) { _template["_name"] = Symbol(name); } template - std::string UserType::get_name() + std::string Usertype::get_name() { return _template["_name"].operator std::string(); } template - void UserType::set_mutable(bool b) + void Usertype::set_mutable(bool b) { _template["_is_mutable"] = b; } template - bool UserType::is_mutable() + bool Usertype::is_mutable() { return _template["_is_mutable"]; } template - void UserType::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) + void Usertype::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) { jl_gc_pause; @@ -70,10 +70,10 @@ namespace jluna } template - Any* UserType::box(T in) + Any* Usertype::box(T in) { if (not is_implemented()) - throw UserTypeNotFullyInitializedException(); + throw UsertypeNotFullyInitializedException(); jl_gc_pause; static jl_function_t* set_field = jl_find_function("jluna.usertype", "set_field!"); @@ -93,10 +93,10 @@ namespace jluna } template - T UserType::unbox(Any* in) + T Usertype::unbox(Any* in) { if (not is_implemented()) - throw UserTypeNotFullyInitializedException(); + throw UsertypeNotFullyInitializedException(); jl_gc_pause; static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); @@ -111,7 +111,7 @@ namespace jluna } template - void UserType::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) + void Usertype::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) { jl_gc_pause; static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); @@ -120,7 +120,7 @@ namespace jluna } template - Type UserType::implement(Module module) + Type Usertype::implement(Module module) { jl_gc_pause; @@ -133,41 +133,35 @@ namespace jluna } template - bool UserType::is_implemented() + bool Usertype::is_implemented() { return _implemented; } - template>, Bool>> - U unbox(Any* in) + template + T unbox(Any* in) { - if (not UserType::is_implemented()) - throw UserTypeNotFullyInitializedException(); + if (not Usertype::is_implemented()) + throw UsertypeNotFullyInitializedException(); - return UserType::unbox(in); + return Usertype::unbox(in); } - template>, Bool>> - Any* box(U in) + template + Any* box(T in) { - if (not UserType::is_implemented()) - throw UserTypeNotFullyInitializedException(); + if (not Usertype::is_implemented()) + throw UsertypeNotFullyInitializedException(); - return UserType::box(in); - } - - template - Any* box(UserTypeWrapper wrapper) - { - UserType::box(wrapper.get()); + return Usertype::box(in); } namespace detail { template - struct to_julia_type_aux> + struct to_julia_type_aux> { - // usertype name is only available, after UserType::UserType(T) was called at least once + // usertype name is only available, after Usertype::Usertype(T) was called at least once static inline std::string type_name = ""; }; } diff --git a/.test/main.cpp b/.test/main.cpp index 266449e..f9c8a31 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -16,15 +16,45 @@ struct NonJuliaType { public: NonJuliaType() = default; + NonJuliaType(Int64 in) + : _member(in) + {} + + void set_member(Int64 in) + { + _member = in; + } + + Int64 get_member() const + { + return _member; + } private: Int64 _member; }; - +using namespace jluna; int main() { State::initialize(); + std::cout << IsUsertypeDbg << std::endl; + return 0; + + Usertype::enable("NonJuliaType"); + Usertype::add_field( + "_member", + "Int64", + [](NonJuliaType& in) -> Any* {return box(in.get_member());}, + [](NonJuliaType& out, Any* value) {out.set_member(unbox(value));} + ); + + Usertype::implement(); + + //State::new_named_undef("julia_side_instance") = box(NonJuliaType(999)); + //jl_eval_string("println(julia_side_instance)"); + + return 0; Test::initialize(); Test::test("catch c exception", [](){ diff --git a/docs/manual.md b/docs/manual.md index 2487100..6cc73e8 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -271,7 +271,7 @@ std::function => function (::Any, ::Any, ::Any) -> std::function => function (::Any, ::Any, ::Any, ::Any) -> Any std::function)> => function (::Vector{Any}) ::Any -UserType => T * +Usertype => T * * where T is an arbitrary C++ type ``` @@ -1839,7 +1839,7 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes -So far, we could only exchange information between the Julia and C++ state if its type was (un)boxable. This list of types while broad, is limited. This is why `jluna` offers an interface that allows users to specify their own (un)boxing routines and make *arbitrary* C++ types (un)boxable. We do this through a wrapper type `jluna::UserType`. +So far, we could only exchange information between the Julia and C++ state if its type was (un)boxable. This list of types while broad, is limited. This is why `jluna` offers an interface that allows users to specify their own (un)boxing routines and make *arbitrary* C++ types (un)boxable. We do this through a wrapper type `jluna::Usertype`. Unlike the previous sections, it may be best to illustrate the use of the usertype interface through an example. Let's say we want the following C++ class to be (un)boxable: diff --git a/include/box.hpp b/include/box.hpp index b413c2d..ccc1c97 100644 --- a/include/box.hpp +++ b/include/box.hpp @@ -186,19 +186,8 @@ namespace jluna Any* box(T); /// @brief box usertype wrapper to usertype - template - class UserType; - - template>, Bool> = true> - Any* box(U in); - - template - class UserTypeWrapper; - - template - Any* box(UserTypeWrapper); + template + Any* box(T); /// @concept requires a value to be boxable into a julia-side value template diff --git a/include/concepts.hpp b/include/concepts.hpp index e292d09..90dc078 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -79,13 +79,26 @@ namespace jluna not Is; /// @concept is std::complex + template + concept IsComplexAux = requires(T t) + { + typename T::value_type; + }; template concept IsComplex = std::is_same_v>; + + /// @concept is std::vector template concept IsVector = std::is_same_v>; + template + concept IsVectorAux = requires(T t) + { + typename T::value_type; + }; + /// @concept is map template concept IsMap = @@ -93,14 +106,34 @@ namespace jluna std::is_same_v> or std::is_same_v>; + template + concept IsMapAux = requires(T t) + { + typename T::key_type; + typename T::mapped_type; + }; + /// @concept is std::set template concept IsSet = std::is_same_v>; + template + concept IsSetAux = requires(T t) + { + typename T::value_type; + }; + /// @concept is pair template concept IsPair = std::is_same_v> and std::tuple_size_v == 2; + template + concept IsPairAux = requires(T t) + { + typename T::first_type; + typename T::second_type; + }; + /// @concept is tuple namespace detail { @@ -113,17 +146,36 @@ namespace jluna template concept IsTuple = detail::is_tuple_aux(std::make_index_sequence::value>()); + template + concept IsTupleAux = requires(T t) + { + {std::tuple_size::value}; + }; /// @concept not unboxable out-of-the-box template - concept IsUsertype = - not IsJuliaValuePointer and - not IsAnyPtrCastable and - not IsPrimitive and - not IsComplex and - not IsVector and - not IsMap and - not IsSet and - not IsPair and - not IsTuple; + concept IsUsertype = requires (T t) + { + not IsJuliaValuePointer; + not IsAnyPtrCastable; + not IsPrimitive; + not IsComplexAux or not IsComplex; + not IsVectorAux or not IsVector; + not IsSetAux or not IsSet; + not IsPairAux or not IsPair; + not IsTupleAux or not IsTuple; + }; + + template + concept IsUsertypeDbg = requires (T t) + { + //not IsJuliaValuePointer; + //not IsAnyPtrCastable; + //not IsPrimitive; + not IsComplexAux || (not IsComplex); + //not IsVectorAux or not IsVector; + //not IsSetAux or not IsSet; + //not IsPairAux or not IsPair; + //not IsTupleAux or not IsTuple; + }; } \ No newline at end of file diff --git a/include/jluna.jl b/include/jluna.jl index b376838..c8ddd86 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -959,14 +959,14 @@ module jluna end """ - interface for jluna::UserType + interface for jluna::Usertype """ module usertype """ usertype wrapper """ - mutable struct UserType + mutable struct Usertype _name::Symbol _is_mutable::Bool @@ -974,7 +974,7 @@ module jluna _field_values::Dict{Symbol, Any} _parameters::Vector{TypeVar} - UserType(name::Symbol, is_mutable::Bool = true) = new( + Usertype(name::Symbol, is_mutable::Bool = true) = new( name, is_mutable, Dict{Symbol, Type}(), @@ -982,35 +982,35 @@ module jluna Vector{TypeVar}() ) end - export UserType + export Usertype """ - `new_usertype(::Symbol) -> UserType` + `new_usertype(::Symbol) -> Usertype` create new usertype """ - function new_usertype(name::Symbol) ::UserType - return UserType(name) + function new_usertype(name::Symbol) ::Usertype + return Usertype(name) end export new_usertype """ - `set_mutable(::UserType, ::Bool) -> Nothing` + `set_mutable(::Usertype, ::Bool) -> Nothing` change mutability of usertype """ - function set_mutable!(x::UserType, value::Bool) ::Nothing + function set_mutable!(x::Usertype, value::Bool) ::Nothing x._is_mutable = value return nothing end export set_mutable! """ - `add_field!(::UserType, name::Symbol, typename::Symbol) -> Nothing` + `add_field!(::Usertype, name::Symbol, typename::Symbol) -> Nothing` add field to usertype, can also be a function """ - function add_field!(x::UserType, name::Symbol, type::Symbol) ::Nothing + function add_field!(x::Usertype, name::Symbol, type::Symbol) ::Nothing x._field_types[name] = type x._field_values[name] = missing @@ -1019,11 +1019,11 @@ module jluna export add_field! """ - `set_field!(::UserType, ::Symbol, value) -> Nothing` + `set_field!(::Usertype, ::Symbol, value) -> Nothing` set value of field in usertype """ - function set_field!(x::UserType, name::Symbol, value) ::Nothing + function set_field!(x::Usertype, name::Symbol, value) ::Nothing @assert haskey(x._field_values, name) x._field_values[name] = value @@ -1031,22 +1031,22 @@ module jluna end """ - `add_parameter!(::UserType, ::Symbol, upper_bound::Type, lower_bound::Type) -> Nothing` + `add_parameter!(::Usertype, ::Symbol, upper_bound::Type, lower_bound::Type) -> Nothing` add parameter, including upper and lower bounds """ - function add_parameter!(x::UserType, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing + function add_parameter!(x::Usertype, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing push!(x._parameters, TypeVar(name, lb, ub)) return nothing end export add_parameter! """ - `implement(::UserType) -> Type` + `implement(::Usertype) -> Type` evaluate the type """ - function implement(type::UserType, m::Module = Main) + function implement(type::Usertype, m::Module = Main) parameters = :() @@ -1076,7 +1076,7 @@ module jluna ctor::Expr = :() if isempty(type._parameters) - ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.UserType)), Expr(:call, :new)) + ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.Usertype)), Expr(:call, :new)) else curly_new = Expr(:curly, :new); for t in type._parameters @@ -1092,7 +1092,7 @@ module jluna type._name, (collect(p.name for p in type._parameters)...) ), - :(base::jluna.usertype.UserType) + :(base::jluna.usertype.Usertype) ), (collect(p.name for p in type._parameters)...) ) diff --git a/include/unbox.hpp b/include/unbox.hpp index 30e6329..8db6760 100644 --- a/include/unbox.hpp +++ b/include/unbox.hpp @@ -143,13 +143,8 @@ namespace jluna T unbox(Any* value); /// @brief unbox usertype wrapper to usertype - template - class UserType; - - template>, Bool> = true> - U unbox(Any* in); + template + T unbox(Any* value); /// @concept requires a value to be unboxed from a julia-side value template diff --git a/include/usertype.hpp b/include/usertype.hpp index 1feb881..1a02c63 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -15,7 +15,7 @@ namespace jluna /// @brief customizable wrapper for non-julia type T /// @note for information on how to use this class, visit https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes template - class UserType + class Usertype { static inline std::function noop_set = [](T&, Any* ) {return;}; @@ -24,7 +24,7 @@ namespace jluna using original_type = T; /// @brief instance - UserType(const std::string& name); + static void enable(const std::string& name); /// @brief set julia-side name /// @param name @@ -84,23 +84,14 @@ namespace jluna >> _mapping = {}; }; - /// @brief wrapper - template - struct UserTypeWrapper : public std::reference_wrapper - { - UserTypeWrapper(T& in) - : std::reference_wrapper(in) - {} - }; - /// @brief exception thrown when usertype is used before being implemented template - struct UserTypeNotFullyInitializedException : public std::exception + struct UsertypeNotFullyInitializedException : public std::exception { public: /// @brief ctor /// @param name - UserTypeNotFullyInitializedException(); + UsertypeNotFullyInitializedException(); /// @brief what /// @returns message From 5d50709a7273944c35e1415c71fbf286edb64a2e Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 19:56:20 +0100 Subject: [PATCH 26/56] stashing --- .src/unbox.inl | 1 - .test/main.cpp | 17 ++++++- include/concepts.hpp | 114 +++++++++++++++---------------------------- 3 files changed, 55 insertions(+), 77 deletions(-) diff --git a/.src/unbox.inl b/.src/unbox.inl index d48ed0d..31a5ed0 100644 --- a/.src/unbox.inl +++ b/.src/unbox.inl @@ -240,7 +240,6 @@ namespace jluna template T> T unbox(Any*); - template T unbox(Any* value) { diff --git a/.test/main.cpp b/.test/main.cpp index f9c8a31..67a09cf 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -38,9 +38,21 @@ using namespace jluna; int main() { State::initialize(); - std::cout << IsUsertypeDbg << std::endl; - return 0; + Usertype::enable("NonJuliaType"); + Usertype::add_field( + "_member", + "Int64", + [](NonJuliaType& in) -> Any* {return box(in.get_member());}, + [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} + ); + Usertype::implement(); + + jluna::safe_eval("julia_side_instance = undef"); + Main["julia_side_instance"] = NonJuliaType(1234); + jluna::safe_eval("println(julia_side_instance"); + + /* Usertype::enable("NonJuliaType"); Usertype::add_field( "_member", @@ -1127,6 +1139,7 @@ int main() i += 1; } }); + */ Test::conclude(); } diff --git a/include/concepts.hpp b/include/concepts.hpp index 90dc078..16ae3ea 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -61,79 +61,66 @@ namespace jluna /// @concept is primitive template concept IsPrimitive = - not Is and - not Is> and - not Is> and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is and - not Is; + Is or + Is> or + Is> or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is or + Is; /// @concept is std::complex - template - concept IsComplexAux = requires(T t) + template + concept IsComplex = requires(T t) { typename T::value_type; + std::is_same_v>; }; - template - concept IsComplex = std::is_same_v>; - - /// @concept is std::vector - template - concept IsVector = std::is_same_v>; - template - concept IsVectorAux = requires(T t) + concept IsVector = requires (T t) { typename T::value_type; + std::is_same_v>; }; /// @concept is map - template - concept IsMap = - std::is_same_v> or - std::is_same_v> or - std::is_same_v>; - template - concept IsMapAux = requires(T t) + concept IsMap = requires(T t) { typename T::key_type; typename T::mapped_type; + std::is_same_v> or + std::is_same_v> or + std::is_same_v>; }; /// @concept is std::set - template - concept IsSet = std::is_same_v>; - template - concept IsSetAux = requires(T t) + concept IsSet = requires(T t) { typename T::value_type; + std::is_same_v>; }; /// @concept is pair - template - concept IsPair = std::is_same_v> and std::tuple_size_v == 2; - template - concept IsPairAux = requires(T t) + concept IsPair = requires(T) { - typename T::first_type; - typename T::second_type; + std::is_same_v>; }; + /// @concept is tuple namespace detail { @@ -146,36 +133,15 @@ namespace jluna template concept IsTuple = detail::is_tuple_aux(std::make_index_sequence::value>()); + /// @concept is none of the above template - concept IsTupleAux = requires(T t) - { - {std::tuple_size::value}; - }; - - /// @concept not unboxable out-of-the-box - template - concept IsUsertype = requires (T t) - { - not IsJuliaValuePointer; - not IsAnyPtrCastable; - not IsPrimitive; - not IsComplexAux or not IsComplex; - not IsVectorAux or not IsVector; - not IsSetAux or not IsSet; - not IsPairAux or not IsPair; - not IsTupleAux or not IsTuple; - }; - - template - concept IsUsertypeDbg = requires (T t) - { - //not IsJuliaValuePointer; - //not IsAnyPtrCastable; - //not IsPrimitive; - not IsComplexAux || (not IsComplex); - //not IsVectorAux or not IsVector; - //not IsSetAux or not IsSet; - //not IsPairAux or not IsPair; - //not IsTupleAux or not IsTuple; - }; + concept IsUsertype = + not IsJuliaValuePointer + and not IsAnyPtrCastable + and not IsPrimitive + and not IsComplex + and not IsVector + and not IsSet + and not IsPair + and not IsTuple; } \ No newline at end of file From 3fc11d31df791f5a468a15e168f6417a20720594 Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 20:29:10 +0100 Subject: [PATCH 27/56] polish --- .test/main.cpp | 23 +++++++++++------------ include/concepts.hpp | 6 +----- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index 67a09cf..f4b842a 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -39,18 +39,17 @@ int main() { State::initialize(); - Usertype::enable("NonJuliaType"); - Usertype::add_field( - "_member", - "Int64", - [](NonJuliaType& in) -> Any* {return box(in.get_member());}, - [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} - ); - Usertype::implement(); - - jluna::safe_eval("julia_side_instance = undef"); - Main["julia_side_instance"] = NonJuliaType(1234); - jluna::safe_eval("println(julia_side_instance"); +Usertype::enable("NonJuliaType"); +Usertype::add_field( + "_member", // field name + Int64, // field type + [](NonJuliaType& in) -> Any* {return box(in.get_member());}, // getter during box + [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} // setter during unbox +); +Usertype::implement(); // push type to julia + +jluna::safe_eval("julia_side_instance = undef"); +Main["julia_side_instance"] = NonJuliaType(1234); // works now /* Usertype::enable("NonJuliaType"); diff --git a/include/concepts.hpp b/include/concepts.hpp index 16ae3ea..dd2709d 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -18,10 +18,6 @@ namespace jluna template concept Is = std::is_same::value or std::is_same_v; - /// @concept: is tuple but not pair - //template - //concept IsTuple = std::tuple_size::value != 2; - /// @concept: has default ctor template concept IsDefaultConstructible = requires(T) @@ -120,7 +116,6 @@ namespace jluna std::is_same_v>; }; - /// @concept is tuple namespace detail { @@ -142,6 +137,7 @@ namespace jluna and not IsComplex and not IsVector and not IsSet + and not IsMap and not IsPair and not IsTuple; } \ No newline at end of file From 11365a522ecf60ae74bb624a5b5b772a92f457c7 Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 20:31:24 +0100 Subject: [PATCH 28/56] added default constructible requirement to usertype --- include/concepts.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/concepts.hpp b/include/concepts.hpp index dd2709d..40cdc18 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -48,6 +48,7 @@ namespace jluna std::is_same_v or std::is_same_v; + /// @concept can be cast to Any* template concept IsAnyPtrCastable = requires(T t) { @@ -128,10 +129,11 @@ namespace jluna template concept IsTuple = detail::is_tuple_aux(std::make_index_sequence::value>()); - /// @concept is none of the above + /// @concept is none of the above and default constructible template concept IsUsertype = - not IsJuliaValuePointer + IsDefaultConstructible + and not IsJuliaValuePointer and not IsAnyPtrCastable and not IsPrimitive and not IsComplex From 161f7bbc69cbeb8fa28b681269cf66dd0c930185 Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 21:51:27 +0100 Subject: [PATCH 29/56] removing parameters for usertypes --- .src/state.cpp | 2 +- .src/type.cpp | 2 +- .src/usertype.inl | 62 +++++++++++++++++++++++++++++++++----------- .test/main.cpp | 6 +++-- include/jluna.jl | 23 ++++++++++++++++ include/usertype.hpp | 55 ++++++++++++++++++++++++++++++++------- 6 files changed, 121 insertions(+), 29 deletions(-) diff --git a/.src/state.cpp b/.src/state.cpp index 19811ca..0e6ea30 100644 --- a/.src/state.cpp +++ b/.src/state.cpp @@ -285,7 +285,7 @@ namespace jluna::State::detail UndefInitializer_t = Type(unroll("Core.UndefInitializer")); Union_t = Type(unroll("Core.Union")); UnionAll_t = Type(unroll("Core.UnionAll")); - //UnionEmpty_t = Type((jl_datatype_t*) jl_eval_string("return Union{}")); + UnionEmpty_t = Type(unroll("Union{}")); Unsigned_t = Type(unroll("Core.Unsigned")); VecElement_t = Type(unroll("Core.VecElement")); WeakRef_t = Type(unroll("Core.WeakRef")); diff --git a/.src/type.cpp b/.src/type.cpp index 2f46274..7e25990 100644 --- a/.src/type.cpp +++ b/.src/type.cpp @@ -11,7 +11,7 @@ namespace jluna Type::Type() = default; Type::Type(jl_datatype_t* value) - : Proxy((jl_value_t*) value, value->name->name) + : Proxy((jl_value_t*) value, (value->name == NULL ? jl_symbol("Union{}") : value->name->name)) {} Type::Type(Proxy* owner) diff --git a/.src/usertype.inl b/.src/usertype.inl index 8f5aead..88bfc94 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -11,9 +11,12 @@ namespace jluna UsertypeNotFullyInitializedException::UsertypeNotFullyInitializedException() { std::stringstream str; - str << "[C++][EXCEPTION] Usertype interface for this type has not yet been implemented, make sure that the following actions were performed before calling (un)box:" << std::endl; - str << "\ta) Usertype::Usertype(const std::string&) was used instance the usertype interface at least once" << std::endl; - str << "\tb) After calling the ctor, Usertype::implement was called exactly once to push the usertype interface to the julia state" << std::endl; + str << "\n[C++][EXCEPTION] Usertype interface for this type T has not yet been fully implemented, make sure the following are true:" << std::endl; + str << "\t1) T is default-constructible and a an implementation is available at compile time" << std::endl; + str << "\t2) Usertype::enable(\"\") was used to instance the usertype interface" << std::endl; + str << "\t3) Usertype::implement() was called, after which the interface cannot be extended" << std::endl; + str << "\nIf all of the above are true, T is now (un)boxable and to_julia_type::type_name is defined." << std::endl; + str << "(for more information, visit: https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes)" << std::endl; _msg = str.str(); } @@ -23,21 +26,31 @@ namespace jluna return _msg.c_str(); } + template + UsertypeAlreadyImplementedException::UsertypeAlreadyImplementedException() + { + std::stringstream str; + str << "[C++][EXCEPTION] Usertype::implement() was already called once for this type. It cannot be extended afterwards." << std::endl; + _msg = str.str(); + } + + template + const char * UsertypeAlreadyImplementedException::what() const noexcept + { + return _msg.c_str(); + } + template void Usertype::enable(const std::string& name) { + if (_implemented) + throw UsertypeAlreadyImplementedException(); + jl_gc_pause; static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str()))); detail::to_julia_type_aux>::type_name = name; jl_gc_unpause; - set_name(name); - } - - template - void Usertype::set_name(const std::string& name) - { - _template["_name"] = Symbol(name); } template @@ -49,6 +62,9 @@ namespace jluna template void Usertype::set_mutable(bool b) { + if (_implemented) + throw UsertypeAlreadyImplementedException(); + _template["_is_mutable"] = b; } @@ -61,8 +77,10 @@ namespace jluna template void Usertype::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) { - jl_gc_pause; + if (_implemented) + throw UsertypeAlreadyImplementedException(); + jl_gc_pause; static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); auto res = (*(_mapping.insert({name, {type, box_get, unbox_set}})).first); jluna::safe_call(add_field, _template, jl_symbol(res.first.c_str()), jl_symbol(std::get<0>(res.second).c_str())); @@ -70,7 +88,13 @@ namespace jluna } template - Any* Usertype::box(T in) + void Usertype::add_field(const std::string& name, Type& type, std::function box_get, std::function unbox_set) + { + add_field(name, type.operator std::string(), box_get, unbox_set); + } + + template + Any* Usertype::box(T& in) { if (not is_implemented()) throw UsertypeNotFullyInitializedException(); @@ -111,22 +135,30 @@ namespace jluna } template - void Usertype::add_parameter(const std::string& name, Type upper_bound, Type lower_bound) + void Usertype::add_parameter(const std::string& name, const Type& upper_bound, const Type& lower_bound) { + if (_implemented) + throw UsertypeAlreadyImplementedException(); + jl_gc_pause; static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); - jluna::safe_call(add_parameter,_template, jl_symbol(name.c_str()), upper_bound, lower_bound); + jluna::safe_call(add_parameter, _template, jl_symbol(name.c_str()), upper_bound.operator const Any*(), lower_bound.operator const Any*()); jl_gc_unpause; } template Type Usertype::implement(Module module) { - jl_gc_pause; + if (_implemented) + throw UsertypeAlreadyImplementedException(); + jl_gc_pause; static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); _implemented_type = jluna::safe_call(implement, _template, module); _implemented = true; + + auto* out = jl_call1(_implemented_type, _template); + forward_last_exception(); jl_gc_unpause; return Type((jl_datatype_t*) _implemented_type); diff --git a/.test/main.cpp b/.test/main.cpp index f4b842a..b474c04 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -38,11 +38,12 @@ using namespace jluna; int main() { State::initialize(); - Usertype::enable("NonJuliaType"); + +Usertype::add_parameter("T", Integer_t); Usertype::add_field( "_member", // field name - Int64, // field type + "T", // field type [](NonJuliaType& in) -> Any* {return box(in.get_member());}, // getter during box [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} // setter during unbox ); @@ -50,6 +51,7 @@ Usertype::implement(); // push type to julia jluna::safe_eval("julia_side_instance = undef"); Main["julia_side_instance"] = NonJuliaType(1234); // works now +jluna::safe_eval("println(julia_side_instance)"); /* Usertype::enable("NonJuliaType"); diff --git a/include/jluna.jl b/include/jluna.jl index c8ddd86..fae5f7a 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1074,9 +1074,15 @@ module jluna end ctor::Expr = :() + default_ctor::Expr = :() if isempty(type._parameters) ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.Usertype)), Expr(:call, :new)) + + default_ctor = Expr(:(=), Expr(:call, type._name), Expr(:call, :new)); + for (_, field_value) in type._field_values + push!(default_ctor.args[2].args, field_value) + end else curly_new = Expr(:curly, :new); for t in type._parameters @@ -1098,6 +1104,21 @@ module jluna ) ctor = Expr(:(=), where_call, Expr(:call, curly_new)) + + where_call = Expr( + :where, + Expr( + :call, + Expr( + :curly, + type._name, + (collect(p.name for p in type._parameters)...) + ) + ), + (collect(p.name for p in type._parameters)...) + ) + + default_ctor = Expr(:(=), where_call, Expr(:call, curly_new, (collect(missing for _ in 1:length(type._parameters))...))) end for (field_name, field_value) in type._field_values @@ -1106,6 +1127,8 @@ module jluna end push!(block.args, ctor) + push!(block.args, default_ctor); + out::Expr = :(mutable struct $(parameters) end) out.args[3] = block diff --git a/include/usertype.hpp b/include/usertype.hpp index 1a02c63..655aa85 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -23,19 +23,17 @@ namespace jluna /// @brief original type using original_type = T; - /// @brief instance + /// @brief enable this type by giving it a name + /// @param name: julia-side name static void enable(const std::string& name); - /// @brief set julia-side name - /// @param name - static void set_name(const std::string& name); - /// @brief get julia-side name /// @returns name static std::string get_name(); /// @brief set mutability, no by default /// @param bool + /// @note this function will throw if called after implement() static void set_mutable(bool); /// @brief get mutability @@ -43,6 +41,11 @@ namespace jluna static bool is_mutable(); /// @brief add field + /// @param name: julia-side name of field + /// @param type_name: symbol of the fields type, such as "Int64" or "P" (where P is a parameter) + /// @param box_get: lambda with signature (T&) -> Any* + /// @param unbox_set: lambda with signature (T&, Any*) + /// @note this function will throw if called after implement() static void add_field( const std::string& name, const std::string& type_name, @@ -50,11 +53,26 @@ namespace jluna std::function unbox_set = noop_set ); + /// @brief add field + /// @param name: julia-side name of field + /// @param type: type of symbol. User the other overload if the type is a typevar, such as "P" (where P is a parameter) + /// @param box_get: lambda with signature (T&) -> Any* + /// @param unbox_set: lambda with signature (T&, Any*) + /// @note this function will throw if called after implement() + static void add_field( + const std::string& name, + Type& type, + std::function box_get, + std::function unbox_set = noop_set + ); + /// @brief add parameter - /// @param name: e.g. T - /// @param upper_bound: .ub of TypeVar, equivalent to T <: upper_bound - /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: T - static void add_parameter(const std::string& name, Type upper_bound = Any_t, Type lower_bound = UnionEmpty_t); + /// @param name: e.g. "P" + /// @param upper_bound: .ub of TypeVar, equivalent to P <: upper_bound + /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: P + /// @param unbox_set: lambda with signature (T&, Any*) + /// @note this function will throw if called after implement() + static void add_parameter(const std::string& name, const Type& upper_bound = Any_t, const Type& lower_bound = UnionEmpty_t); /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in @@ -66,7 +84,7 @@ namespace jluna static bool is_implemented(); /// @brief box interface - static Any* box(T); + static Any* box(T&); /// @brief unbox interface static T unbox(Any*); @@ -100,6 +118,23 @@ namespace jluna private: std::string _msg; }; + + /// @brief exception thrown when usertype is used before being implemented + template + struct UsertypeAlreadyImplementedException : public std::exception + { + public: + /// @brief ctor + /// @param name + UsertypeAlreadyImplementedException(); + + /// @brief what + /// @returns message + const char* what() const noexcept override final; + + private: + std::string _msg; + }; } #include ".src/usertype.inl" \ No newline at end of file From f3770e78d4c41d9ba59d3b0b480b97f7062c80ec Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 21:54:29 +0100 Subject: [PATCH 30/56] todo: remove parameters --- .src/usertype.inl | 22 ++-------------------- .test/main.cpp | 4 +--- include/jluna.jl | 2 +- include/usertype.hpp | 23 +---------------------- 4 files changed, 5 insertions(+), 46 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 88bfc94..ab47570 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -75,7 +75,7 @@ namespace jluna } template - void Usertype::add_field(const std::string& name, const std::string& type, std::function box_get, std::function unbox_set) + void Usertype::add_field(const std::string& name, const Type& type, std::function box_get, std::function unbox_set) { if (_implemented) throw UsertypeAlreadyImplementedException(); @@ -83,16 +83,10 @@ namespace jluna jl_gc_pause; static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); auto res = (*(_mapping.insert({name, {type, box_get, unbox_set}})).first); - jluna::safe_call(add_field, _template, jl_symbol(res.first.c_str()), jl_symbol(std::get<0>(res.second).c_str())); + jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), type.get_symbol(), jl_symbol(std::get<0>(res.second).c_str())); jl_gc_unpause; } - template - void Usertype::add_field(const std::string& name, Type& type, std::function box_get, std::function unbox_set) - { - add_field(name, type.operator std::string(), box_get, unbox_set); - } - template Any* Usertype::box(T& in) { @@ -134,18 +128,6 @@ namespace jluna return out; } - template - void Usertype::add_parameter(const std::string& name, const Type& upper_bound, const Type& lower_bound) - { - if (_implemented) - throw UsertypeAlreadyImplementedException(); - - jl_gc_pause; - static jl_function_t* add_parameter = jl_find_function("jluna.usertype", "add_parameter!"); - jluna::safe_call(add_parameter, _template, jl_symbol(name.c_str()), upper_bound.operator const Any*(), lower_bound.operator const Any*()); - jl_gc_unpause; - } - template Type Usertype::implement(Module module) { diff --git a/.test/main.cpp b/.test/main.cpp index b474c04..aeef0a9 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -39,11 +39,9 @@ int main() { State::initialize(); Usertype::enable("NonJuliaType"); - -Usertype::add_parameter("T", Integer_t); Usertype::add_field( "_member", // field name - "T", // field type + Int64_t, // field type [](NonJuliaType& in) -> Any* {return box(in.get_member());}, // getter during box [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} // setter during unbox ); diff --git a/include/jluna.jl b/include/jluna.jl index fae5f7a..fd58fa9 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -1010,7 +1010,7 @@ module jluna add field to usertype, can also be a function """ - function add_field!(x::Usertype, name::Symbol, type::Symbol) ::Nothing + function add_field!(x::Usertype, name::Symbol, type::Type) ::Nothing x._field_types[name] = type x._field_values[name] = missing diff --git a/include/usertype.hpp b/include/usertype.hpp index 655aa85..c3e164b 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -40,19 +40,6 @@ namespace jluna /// @returns bool static bool is_mutable(); - /// @brief add field - /// @param name: julia-side name of field - /// @param type_name: symbol of the fields type, such as "Int64" or "P" (where P is a parameter) - /// @param box_get: lambda with signature (T&) -> Any* - /// @param unbox_set: lambda with signature (T&, Any*) - /// @note this function will throw if called after implement() - static void add_field( - const std::string& name, - const std::string& type_name, - std::function box_get, - std::function unbox_set = noop_set - ); - /// @brief add field /// @param name: julia-side name of field /// @param type: type of symbol. User the other overload if the type is a typevar, such as "P" (where P is a parameter) @@ -61,19 +48,11 @@ namespace jluna /// @note this function will throw if called after implement() static void add_field( const std::string& name, - Type& type, + const Type& type, std::function box_get, std::function unbox_set = noop_set ); - /// @brief add parameter - /// @param name: e.g. "P" - /// @param upper_bound: .ub of TypeVar, equivalent to P <: upper_bound - /// @param lower_bound: .lb of TypeVar, equivalent to lower_bound <: P - /// @param unbox_set: lambda with signature (T&, Any*) - /// @note this function will throw if called after implement() - static void add_parameter(const std::string& name, const Type& upper_bound = Any_t, const Type& lower_bound = UnionEmpty_t); - /// @brief push to state and eval, cannot be extended afterwards /// @param module: module the type will be set in /// @returns julia-side type From 8b99537794a3dea971573bb8372dbf47a06c2773 Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 22:37:28 +0100 Subject: [PATCH 31/56] stashing --- .src/usertype.inl | 2 +- include/jluna.jl | 4 +++- include/usertype.hpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index ab47570..58b6733 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -83,7 +83,7 @@ namespace jluna jl_gc_pause; static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); auto res = (*(_mapping.insert({name, {type, box_get, unbox_set}})).first); - jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), type.get_symbol(), jl_symbol(std::get<0>(res.second).c_str())); + jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), std::get<0>(res.second).operator jl_datatype_t*()); jl_gc_unpause; } diff --git a/include/jluna.jl b/include/jluna.jl index fd58fa9..5458032 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -970,7 +970,7 @@ module jluna _name::Symbol _is_mutable::Bool - _field_types::Dict{Symbol, Symbol} + _field_types::Dict{Symbol, Type} _field_values::Dict{Symbol, Any} _parameters::Vector{TypeVar} @@ -1079,6 +1079,8 @@ module jluna if isempty(type._parameters) ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.Usertype)), Expr(:call, :new)) + + TODO: DEFAULT CTOR default_ctor = Expr(:(=), Expr(:call, type._name), Expr(:call, :new)); for (_, field_value) in type._field_values push!(default_ctor.args[2].args, field_value) diff --git a/include/usertype.hpp b/include/usertype.hpp index c3e164b..364be76 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -75,7 +75,7 @@ namespace jluna static inline Any* _implemented_type = nullptr; static inline std::map, // getter std::function // setter >> _mapping = {}; From 0e32a95b34ba08111020dad5374c1be198ab5e8b Mon Sep 17 00:00:00 2001 From: clem Date: Mon, 28 Feb 2022 22:52:30 +0100 Subject: [PATCH 32/56] working --- .src/usertype.inl | 3 --- include/jluna.jl | 15 ++++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 58b6733..e56f49e 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -138,9 +138,6 @@ namespace jluna static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); _implemented_type = jluna::safe_call(implement, _template, module); _implemented = true; - - auto* out = jl_call1(_implemented_type, _template); - forward_last_exception(); jl_gc_unpause; return Type((jl_datatype_t*) _implemented_type); diff --git a/include/jluna.jl b/include/jluna.jl index 5458032..0a96e96 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -557,7 +557,7 @@ module jluna call any function, update the handler then forward the result, if any """ - function safe_call(f::Function, args...) + function safe_call(f::Any, args...) result = undef try @@ -1053,6 +1053,7 @@ module jluna if isempty(type._parameters) parameters = type._name else + @assert false parameters = Expr(:curly, type._name) for tv in type._parameters if tv.lb == Union{} @@ -1077,15 +1078,15 @@ module jluna default_ctor::Expr = :() if isempty(type._parameters) - ctor = Expr(:(=), :($(type._name)(base::jluna.usertype.Usertype)), Expr(:call, :new)) + ctor = Expr(:(=), :($(type._name)(base::Main.jluna.usertype.Usertype)), Expr(:call, :new)) - - TODO: DEFAULT CTOR default_ctor = Expr(:(=), Expr(:call, type._name), Expr(:call, :new)); - for (_, field_value) in type._field_values - push!(default_ctor.args[2].args, field_value) + for (field_name, field_type) in type._field_types + push!(default_ctor.args[1].args, Expr(:(::), field_name, field_type)) + push!(default_ctor.args[2].args, field_name) end else + @assert false curly_new = Expr(:curly, :new); for t in type._parameters push!(curly_new.args, t.name) @@ -1123,7 +1124,7 @@ module jluna default_ctor = Expr(:(=), where_call, Expr(:call, curly_new, (collect(missing for _ in 1:length(type._parameters))...))) end - for (field_name, field_value) in type._field_values + for (field_name, _) in type._field_values field_symbol = QuoteNode(field_name) push!(ctor.args[2].args, :(base._field_values[$(field_symbol)])) end From 847152e0a5244a3f05d76103b87f7fe39deea3d5 Mon Sep 17 00:00:00 2001 From: clem Date: Tue, 1 Mar 2022 01:48:41 +0100 Subject: [PATCH 33/56] stashing manual --- .test/main.cpp | 9 ++- CMakeLists.txt | 2 +- docs/manual.md | 153 ++++++++++++++++++++++++++++++---------- include/gc_sentinel.hpp | 29 ++++++++ include/usertype.hpp | 6 +- jluna.hpp | 3 +- 6 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 include/gc_sentinel.hpp diff --git a/.test/main.cpp b/.test/main.cpp index aeef0a9..e9ca693 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -47,9 +47,16 @@ Usertype::add_field( ); Usertype::implement(); // push type to julia +State::new_named_undef("lambda") = [](Any* in) -> Any* { + + auto unboxed = unbox(in); + unboxed.set_member(unboxed.get_member() + 1); + return box(unboxed); +}; + jluna::safe_eval("julia_side_instance = undef"); Main["julia_side_instance"] = NonJuliaType(1234); // works now -jluna::safe_eval("println(julia_side_instance)"); +jluna::safe_eval("println(lambda(julia_side_instance))"); /* Usertype::enable("NonJuliaType"); diff --git a/CMakeLists.txt b/CMakeLists.txt index e086427..edeb673 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,7 +117,7 @@ add_library(jluna SHARED include/usertype.hpp .src/usertype.inl -) + include/gc_sentinel.hpp) set_target_properties(jluna PROPERTIES LINKER_LANGUAGE C diff --git a/docs/manual.md b/docs/manual.md index 6cc73e8..97a31c8 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1839,78 +1839,157 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes +### Example + So far, we could only exchange information between the Julia and C++ state if its type was (un)boxable. This list of types while broad, is limited. This is why `jluna` offers an interface that allows users to specify their own (un)boxing routines and make *arbitrary* C++ types (un)boxable. We do this through a wrapper type `jluna::Usertype`. Unlike the previous sections, it may be best to illustrate the use of the usertype interface through an example. Let's say we want the following C++ class to be (un)boxable: ```cpp -class Quiver +class Frog { + private: + std::string _name; + public: - struct Arrow + struct Tadpole { - const size_t _unique_id; + Tadpole() = default; + + void set_name(const std::string& name) + { + _name = name; + } + + const std::string& get_name() const + { + return _name; + } + + Frog evolve() + { + assert(_name != ""); + return Frog(name); + } + + private: + std::string _name; + }; - Quiver() - : _holds(); + Frog(const std::string& name) + : _name {} - void add(Arrow arrow) + std::vector spawn(size_t number) { - _holds.push_back(arrow); - } - - Arrow remove() - { - auto out = _holds.front(); - _holds.erase(_holds.begin()); + std::vector out; + for (size_t i = 0; i < number; ++i) + out.push_back(Tadpole()); + return out; } - - size_t size() - { - return _holds.size(); - } - - private: - std::vector _holds; }; ``` -Let's first investigate this class using C++ terminology. Firstly, it is call `Quiver` and has an internal, public class called `Arrow`. `Arrow` only has a single, constant member `_unique_id`. `Quiver` has 3 member functions that add an arrow, remove an error and count the number of arrows. The arrows are held in an internal vector. +Let's first investigate this class. The outer class, `Frog`, only has a single field called `_name`. It has one ctor that takes the name and sets it, after constructing there is no way to renamed the frog. `Frog` other function is `spawn` which returns vector of `Frog::Tadpole`s. `Frog::Tadpole` is an internal class, tadpoles also have a name except they don't when they are born, we will have to name them some time afterwards. We can evolve a tadpole into a frog of the same name using `evolve`, however at that time, the tadpole has to have been named or an assertion is raised. + +When translating this class in Julia, we run into a number of problems. There are no traditional internal classes in julia, if we define a struct inside another struct, both will be available in the outer structs namespace. Julia furthermore has no concept of visiblity, all its members are public. Lastly, julia does not have traditional member functions, we can have members that are functions but that functions definition does not have implicit access to the classes other members, as it would in C++. -When trying to translate this class to julia, we immediately run into a couple of problems. In julia, there are no internal structs in the traditional way. A struct declared inside another struct is available in the same scope. Furthermore, julia does not have traditional member functions, a function that is a member of a struct cannot access any other member in that struct implicitly. We can get around this by employing the following trick: +Despite these differences, if we were to translate this C++ class into Julia, it would be something like: ```julia -# in cpp +mutable struct Tadpole + + _name::String + evolve::Function + + Tadpole() = new("", + function (this::Tadpole) ::Frog + @assert this._name != "" + return Frog(this._name) + end + ) +end +struct Frog -Lastly, julia has no concept similar to private or public members, all members are always public. Given this, one way to translate `Quiver` to julia would be the following: + _name::String + spawn::Function + + Frog(name::String) = new(name, + (this::Frog, number::Integer) -> [Tadpole() for _ in 1:number] + ) +end +``` +Let's talk through why this is a faithful translation. Regarding `Tadpole`, in C++ we had a setter `set_name` and getter `get_name` for the property `_name`, this for the julia struct, we need to both be able to read and write to `_name`. By making `Tadpole` mutable, this is achieved. `Tadpole` is furthermore "default constructible" (in C++ parlance), that is, it has a constructor that takes no arguments and initializes the `_name` property as an empty string. Lastly we come to `Tadpole::evolve`, because julia doesn't have traditional member functions we employ the following design: ```julia -struct Arrow - _unique_id::UInt64 +# in cpp: +Frog Tadpole::evolve() +{ + assert((*this)._name != ""); + return Frog((*this)._name); +} + +#in julia +function Tadpole.evolve(this::Tadpole) ::Frog + @assert this._name != "" + return Frog(this._name) end +``` +By making the classes instance an argument for the member function, we can simulate the behavior of a C++ member function. Calling both functions looks very similar: -struct Quiver - - add::Function - remove::Function - size::Function - - _holds::Vector{Arrow} +```julia +# in cpp +Tadpole instance = Tadpole(); +instance.evolve(); // equivalent to: instance.Tadpole::evolve() + +# in julia +instance::Tadpole = Tadpole(); +Tadpole.evolve(instance); +``` + +Using this trick, we can simulate C++ syntax very closely. It is paramount to understand this, as it will be the basis for the rest of the section. + +Regarding the julia version of `Frog` now: In C++, the _name property can bread (via `get_name`) but cannot be set, we cannot renamed a frog after it evolved. To simulate this behavior, we make the julia-side frog an immutable struct. For the member function `spawn`, we utilize the same trick as with `Tadpole`: + +```julia +# in cpp +std::vector Frog::spawn(size_t number) +{ + std::vector out; + for (size_t i = 0; i < number; ++i) + out.push_back(Tadpole()); + + return out; +} + +# in julia +function Frog.spawn(number::Integer) ::Vector{Tadpole} - Quiver() = new( - function (arrow::Arrow) + out = Vector{Tadpole}() + for i in 1:number + push!(out, Tadpole()) + end - ) + return out end +``` + +Really, the only difference is that in julia, we have to define the member function during construction, rather than during struct declaration. + +### Usertype Interface +Now that we know how to translate the classes, one might ask: how do we exchange memory from one state to another? Neither `Frog` nor `Tadpole` were listed in the number of types that are (un)boxable, so we will have to define our own unboxing routines, right? No, we do not. `jluna` understands that this requires an unreasonable amount of effort, due to how cumbersome the C-API is to use, so instead it gives us a centralized usertype interface: `Usertype`. + +For the interface to be able to handle a type T ``` + + --- ## Performance diff --git a/include/gc_sentinel.hpp b/include/gc_sentinel.hpp new file mode 100644 index 0000000..7c6b7e6 --- /dev/null +++ b/include/gc_sentinel.hpp @@ -0,0 +1,29 @@ +// +// Copyright 2022 Clemens Cords +// Created on 01.03.22 by clem (mail@clemens-cords.com) +// + +#pragma once + +#include + +namespace jluna +{ + /// @brief convenient, lock-like class that protects from the garbage collector while it is in scope + struct GCSentinel + { + private: + bool _before; + + GCSentinel() + { + _before = jl_gc_is_enabled(); + jl_gc_enable(false); + } + + ~GCSentinel() + { + jl_gc_enable(_before); + } + }; +} \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 364be76..ec4b817 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -75,7 +75,7 @@ namespace jluna static inline Any* _implemented_type = nullptr; static inline std::map, // getter std::function // setter >> _mapping = {}; @@ -87,7 +87,6 @@ namespace jluna { public: /// @brief ctor - /// @param name UsertypeNotFullyInitializedException(); /// @brief what @@ -98,13 +97,12 @@ namespace jluna std::string _msg; }; - /// @brief exception thrown when usertype is used before being implemented + /// @brief exception thrown implementation is called twice template struct UsertypeAlreadyImplementedException : public std::exception { public: /// @brief ctor - /// @param name UsertypeAlreadyImplementedException(); /// @brief what diff --git a/jluna.hpp b/jluna.hpp index 2b02481..dbf7d48 100644 --- a/jluna.hpp +++ b/jluna.hpp @@ -20,4 +20,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file From 4ae9bdfebfb7e3f4f21f226452e5ea0b79887c20 Mon Sep 17 00:00:00 2001 From: clem Date: Tue, 1 Mar 2022 17:43:48 +0100 Subject: [PATCH 34/56] stashing --- .test/main.cpp | 84 ++++++++-------- docs/manual.md | 224 +++++++++++++++++++++++++++++++++++++++++++ include/concepts.hpp | 3 +- 3 files changed, 263 insertions(+), 48 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index e9ca693..5e736e5 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,68 +11,60 @@ using namespace jluna; using namespace jluna::detail; -// julia incompatible arbitrary type -struct NonJuliaType +class Frog { + private: + std::string _name; + public: - NonJuliaType() = default; - NonJuliaType(Int64 in) - : _member(in) + struct Tadpole + { + Tadpole() = default; + + void set_name(const std::string& name) + { + _name = name; + } + + const std::string& get_name() const + { + return _name; + } + + Frog evolve() + { + assert(_name != ""); + return Frog(_name); + } + + private: + std::string _name; + }; + + Frog(const std::string& name) + : _name(name) {} - void set_member(Int64 in) + std::vector spawn(size_t number) { - _member = in; - } + std::vector out; + for (size_t i = 0; i < number; ++i) + out.push_back(Tadpole()); - Int64 get_member() const - { - return _member; + return out; } - - private: - Int64 _member; }; using namespace jluna; int main() { State::initialize(); -Usertype::enable("NonJuliaType"); -Usertype::add_field( - "_member", // field name - Int64_t, // field type - [](NonJuliaType& in) -> Any* {return box(in.get_member());}, // getter during box - [](NonJuliaType& out, Any* in) -> void {out.set_member(unbox(in));} // setter during unbox -); -Usertype::implement(); // push type to julia - -State::new_named_undef("lambda") = [](Any* in) -> Any* { - - auto unboxed = unbox(in); - unboxed.set_member(unboxed.get_member() + 1); - return box(unboxed); -}; - -jluna::safe_eval("julia_side_instance = undef"); -Main["julia_side_instance"] = NonJuliaType(1234); // works now -jluna::safe_eval("println(lambda(julia_side_instance))"); - - /* - Usertype::enable("NonJuliaType"); - Usertype::add_field( - "_member", - "Int64", - [](NonJuliaType& in) -> Any* {return box(in.get_member());}, - [](NonJuliaType& out, Any* value) {out.set_member(unbox(value));} - ); - - Usertype::implement(); - //State::new_named_undef("julia_side_instance") = box(NonJuliaType(999)); - //jl_eval_string("println(julia_side_instance)"); + auto cpp_side_instance = Frog("Eve"); + State::new_named_undef("julia_side_instance") = box(cpp_side_instance); return 0; + /* Test::initialize(); Test::test("catch c exception", [](){ diff --git a/docs/manual.md b/docs/manual.md index 97a31c8..3b1dccd 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1896,6 +1896,230 @@ Let's first investigate this class. The outer class, `Frog`, only has a single f When translating this class in Julia, we run into a number of problems. There are no traditional internal classes in julia, if we define a struct inside another struct, both will be available in the outer structs namespace. Julia furthermore has no concept of visiblity, all its members are public. Lastly, julia does not have traditional member functions, we can have members that are functions but that functions definition does not have implicit access to the classes other members, as it would in C++. +### Utilizing Usertypes + +When attempting to transfer frogs and tadpoles between states, one might naively do the following: + +```cpp +using namespace julia +State::initialize(); + +auto cpp_side_instance = Frog("Eve"); +State::new_named_undef("julia_side_instace") = box(cpp_side_instance); +``` + +This is actually valid syntax, it will compile. Executing the above, however, throws at runtime: + +``` +terminate called after throwing an instance of 'jluna::UsertypeNotFullyInitializedException' + what(): +[C++][EXCEPTION] Usertype interface for this type T has not yet been fully implemented, make sure the following are true: + 1) T is default-constructible and a an implementation is available at compile time + 2) Usertype::enable("") was used to instance the usertype interface + 3) Usertype::implement() was called, after which the interface cannot be extended + +If all of the above are true, T is now (un)boxable and to_julia_type::type_name is defined. +``` + +This exception is very verbose and explains to us, what we need to do before a call like `box` can be successfull. Because explaining a process inside a single exception can make things quite dense, we'll go through the process one-by-one. + +### Usertype Interface + +To make an arbitrary type (un)boxable, we can implement its *usertype interface* `jluna::Usertype`. This class is a singleton, meaning there can only ever be one instance of it over the live of the program. Because we need `Tadpole` already (un)boxable to define `Frog`, let's start with the former + +#### Step 1: Enabling the Interface + +To instance the interface as a singleton, we use `Usertype::enable(const std::string&)`. This static function takes a name, this is not the name of the tadpole but the symbol of the type that we will be generating julia side. Most of the time we want the types name to be the same as the C++ class' name: + +```cpp +// in main +Usertype::enable("Tadpole"); +``` + +#### Step 2: Adding Fields + +A type that is just name and nothing else is not very useful, thought. Instead we will need to give it fields that correspond to it's C++ fields. To do this, we use `Usertype::add_field`: + +This method takes 4 arguments: + ++ `const std::string& name`: name of the field julia-side ++ `Type&`: type of the julia-side field, can be `Any_t` ++ `std::function`: a lambda executed during box (see below) ++ `std::functionvoid`: a lambda executed during unbox (see below) + +#### Getter Lambda + +Both the fields name and type are self-explanatory, the lambdas are not. + +When a usertype T is **boxed**, each field added via `add_field` get sassigned a value. This value is the result of the first lambda expression with signature `(T&) -> Any*`, which we will call the **boxing routine** for that specific field. This lambda takes an instance of the type and returns a boxed value. Some examples: + +```cpp +// using a getter to acquire the value for the field +struct A + + std::string get_field() const + { + return _field; + } + + std::string set_field(std::string in) + { + _field = in; + } + + private: + std::string _field; +end + +Usertype::enable("A"); +Usertype::add_field("_field", String_t, [](A& instance) -> Any* { + // call getter on instance and return its value + return box(instance.get_field()); +}); +``` +In this example, during `Any* result_a = box(instance)`, the getter will be called to initialize the value of the field `_field` of `result_a` (which is of type `A`) to `instance.get_field()`. + +```cpp +// default initialize a field +struct B + Int64 _field = 1234; +end + +Usertype::enable("B"); +Usertype::add_field("_field", Int64_t, [](B& _) -> Any* { + // ignore instance and always return the same value + return box(1234); +}); +``` + +In this example, during `Any* result_b = box(instance)` the value of the field `_field` of `result_b` will always be `1234`, regardless of what value the instance holds. This can be thought of as default initialization, however the lambda still needs to conform to the signature `(T&) -> Any*`, which is why we name the argument `_` to signal that it is unused. + +```cpp +// computation unrelated to the instance to set the field +struct C + size_t id; +end + +static inline current_id = 33; + +Usertype::enable("C"); +Usertype::add_field("_id", UInt64_t, [&](B& _) -> Any* { + // capture global id and use it for initialize + current_id += 1; + return box(current_id); +}); +``` +Lastly, here we are using the lambda capture to access the value of `current_id`, which is unrelated to the instance. We increase this value of this global, then box it to hand to the result, such that for the expression `Any* result_c = box(instance)`, the value of the field `_id` will be what we handed it but we also modified a variable unrelated to the instance. + +What these three examples illustrate it similar to how boxing lambdas worked. While we are limited to the signature `(T&) -> Any*`, through captures we can actually execute arbitrary code. + +#### Setter Lambda + +Similar to how the `(T&) -> Any*` getter lambda governs, what the corresponding field of the result of the box expression will be set to, the **unbox routine** governs, what value the result calling `unbox(Any*)` will have. The unbox routine has the following signature: + +`(T& out, Any* field) -> T` + +Where `out` is the resulting object of type T and `field` is the julia-side value of the correponsing field of the julia-side type. Extending our previous examples: + +```cpp +// using a setter to modify the result +struct A + + std::string get_field() const + { + return _field; + } + + std::string set_field(std::string in) + { + _field = in; + } + + private: + std::string _field; +end + +Usertype::enable("A"); +Usertype::add_field( + "_field", // field name + String_t, // field type + // boxing routine + [](A& instance) -> Any* { + return box(instance.get_field()); + }, + // unboxing routine + [](A& instance, Any* field_value) -> void + { + instance.set_field(unbox(field_value)); + return + } +); +``` + +Because `A`s `_field` is private, we can only modify it through `set_field`.`field_value`, here, is the value of the field name `_field`. After unboxing it, we can modify the resulting instance `instance` using the setter. + +```cpp +// default initialize a field +struct B + Int64 _field = 1234; +end + +Usertype::enable("B"); +Usertype::add_field( + "_field", // field name + Int64_t, // field type + // boxing routine + [](B& _) -> Any* { + return box(1234); + }, + // unboxing routine + [](B& out, Any* field_value) -> void + { + out._field = unbox(field_value); + return; + } +); +``` +Here, we want the result of the unbox expression to acquire the value of the julia-side field. Because the member is public, we can simply assign it. + +```cpp +// computation unrelated to the instance to set the field +struct C + size_t id; +end + +static inline current_id = 33; +static inline number_of_cpp_side_instances = 12; + +Usertype::enable("C"); +Usertype::add_field( + "_id", // field name + UInt64_t, // field type + // boxing routine + [&](C& _) -> Any* + { + current_id += 1; + return box(current_id); + }, + // unboxing routine + [&](C& _, Any* _) -> void + + number_of_cpp_side_instnaces + + ); +``` + + + + + + + + + + + + Despite these differences, if we were to translate this C++ class into Julia, it would be something like: ```julia diff --git a/include/concepts.hpp b/include/concepts.hpp index 40cdc18..45e3aeb 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -132,8 +132,7 @@ namespace jluna /// @concept is none of the above and default constructible template concept IsUsertype = - IsDefaultConstructible - and not IsJuliaValuePointer + not IsJuliaValuePointer and not IsAnyPtrCastable and not IsPrimitive and not IsComplex From 03c8f163dbdabb5788f19825718584514469a884 Mon Sep 17 00:00:00 2001 From: clem Date: Tue, 1 Mar 2022 17:59:41 +0100 Subject: [PATCH 35/56] stashing --- docs/manual.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/docs/manual.md b/docs/manual.md index 3b1dccd..ff15722 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1934,7 +1934,70 @@ To instance the interface as a singleton, we use `Usertype::enable(const std: ```cpp // in main Usertype::enable("Tadpole"); +Usertype::enable("Frog"); ``` +Once we finished implementing these types, their will be two julia-side types `Frog` and `Tadpole`, because we gave them these names here. + +#### Step 2 (optional): Setting Mutability + +Next we will need to decide wether we want the julia-side `Frog` and `Tadpole` to be mutable or immutable struct types. Looking at our initial C++ classes, we notice that all member of tadpole, namely just `_name`, can be both read and written to via `set_name` and `get_name`. Because of this, its best not directly translate this behavior via two member function but rather just make the entirety of `Tadpole` mutable: + +```cpp +Usertype::set_mutable(true); +``` + +Frogs name cannot be changed because there is no setter, only a getter. This behavior is identical to an immutable struct type in julia so we should define it as such: + +```cpp +Usertype::set_mutable(false); +``` + +This step is optional because by default, all usertypes are mutable. Usually, however, it is better style to manually declare them so. + +#### Step 3 (optional): Adding Fields & Methods + +To add fields to our julia types, we use `Usertype::add_field`. This class takes 4 arguments: + ++ `const std::string& name`: name of the julia-side field ++ `Type& type`: type of the field ++ `std::function boxing_routine`: this lambda with signature `(T&) -> Any*` will be called during boxing. It's argument of type `T&` is the instance of the object, the argument of the `box(instance)` call. It's result is the value of the corresponding field. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #### Step 2: Adding Fields From b1722e55a87d1d707f8d98b930922161fe8965fc Mon Sep 17 00:00:00 2001 From: clem Date: Tue, 1 Mar 2022 19:05:32 +0100 Subject: [PATCH 36/56] stashing --- .test/main.cpp | 90 ++++++++++++++++++++++++++++++++- docs/manual.md | 118 ++++++++++++++++++++++++++++++++++++++++++- include/concepts.hpp | 10 +++- 3 files changed, 213 insertions(+), 5 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index 5e736e5..1a3a582 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -53,6 +53,11 @@ class Frog return out; } + + std::string get_name() const + { + return _name; + } }; using namespace jluna; @@ -60,8 +65,89 @@ int main() { State::initialize(); - auto cpp_side_instance = Frog("Eve"); - State::new_named_undef("julia_side_instance") = box(cpp_side_instance); + Usertype::enable("Tadpole"); + Usertype::add_field( + "_name", // name of the field + String_t, // type of the field + // boxing routine + [](Frog::Tadpole& in) -> Any* + { + // box field value from cpp-tadpole and send to julia + return box(in.get_name()); + }, + // unboxing routine + [](Frog::Tadpole& out, Any* name_value) -> void + { + // unbox name from julia-tadpole assign cpp-tadpole instance + out.set_name(unbox(name_value)); + return; + } + ); + Usertype::add_field( + "evolve", + Function_t, + [](Frog::Tadpole& in) -> Any* + { + return box([](Any* tadpole_instance) -> Any* { + + Frog::Tadpole in = unbox(tadpole_instance); + return box(Frog(in.get_name())); + }); + }, + [](Frog::Tadpole& _, Any* __) -> void + { + return; + } + ); + + Usertype::enable("Frog"); + Usertype::set_mutable(false); + Usertype::add_field( + "name", + String_t, + [](Frog& in) -> Any* + { + return box(in.get_name()); + }, + [](Frog& out, Any* name) -> void + { + out = Frog(unbox(name)); + return; + } + ); + Usertype::add_field( + "spawn", + Function_t, + [](Frog& in) -> Any* + { + return box([in](Any* frog_instance, Any* number) -> Any* { + + Frog in = unbox(frog_instance); + size_t n = unbox(number); + + return box(in.spawn(n)); + }); + } + ); + + Usertype::implement(); + Usertype::implement(); + + return 0; + + // create cpp-side +auto cpp_tadpole = Frog::Tadpole(); + +// move to julia +State::new_named_undef("jl_tadpole") = cpp_tadpole; // equivalent to `= box(cpp_tadpole)` +jluna::safe_eval("println(jl_tadpole)"); + +// modify +jluna::safe_eval("jl_tadpole._name = \"Ted\""); + +// move back to cpp +cpp_tadpole = Main["jl_tadpole"]; +std::cout << "tadpole is now named : " << cpp_tadpole.get_name() << std::endl; return 0; /* diff --git a/docs/manual.md b/docs/manual.md index ff15722..79f5272 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1958,12 +1958,126 @@ This step is optional because by default, all usertypes are mutable. Usually, ho To add fields to our julia types, we use `Usertype::add_field`. This class takes 4 arguments: -+ `const std::string& name`: name of the julia-side field ++ `const std::string& field_name`: name of the julia-side field + `Type& type`: type of the field -+ `std::function boxing_routine`: this lambda with signature `(T&) -> Any*` will be called during boxing. It's argument of type `T&` is the instance of the object, the argument of the `box(instance)` call. It's result is the value of the corresponding field. ++ `std::function boxing_routine`: lambda with signature `(T&) -> Any*` that will be called during boxing. Its argument, of type `T&`, is the cpp-side instance of type `T` that we're trying to box. The result of this lambda will be assigned to the field `field_name` of the resulting julia-side type ++ `std::function unboxing_routine`: lambda with signature `(T&, Any*) -> void`, that is called during unboxing. The first argument `T&` is the result of the unboxing call. The second argument `Any*` is the current value of the julia-side field `field_name` +Working through our example will be illustrative here. Firstly `Tadpole`: +`Tadpole` has one field `_name` that is of type `std::string`. When boxing, the resulting julia-side tadpole should keep this name. Similarly, when unboxing, the now cpp-side tadpole should keep the name it had julia side. We can thus execute: +```cpp +Usertype::add_field( + "_name", // name of the field + String_t, // type of the field + // boxing routine + [](Frog::Tadpole& in) -> Any* + { + // box field value from cpp-tadpole and send to julia + return box(in.get_name()); + }, + // unboxing routine + [](Frog::Tadpole& out, Any* name_value) -> void + { + // unbox name from julia-tadpole assign cpp-tadpole instance + out.set_name(unbox(name_value)); + return; + } +); +``` + +#### Step 4: Implementing + +All that is left to do, concerning `Frog::Tadpole`, is to push our interface to julia using `Usertype::implement()`. This generates a new type of our specifications for us julia-side and enables (un)boxing for `Frog::Tadpole`: + +```cpp +Usertype::implement(); +``` + +This is not part of the actual release function but for illustration, let's see what jluna executes under-the-hood: + + +```cpp +Usertype::enable("Tadpole"); +Usertype::set_mutable(true) +Usertype::add_field( + "_name", + String_t, + [](Frog::Tadpole& in) -> Any* + { + return box(in.get_name()); + }, + [](Frog::Tadpole& out, Any* name_value) -> void + { + out.set_name(unbox(name_value)); + return; + } +); +Usertype::implement(); +``` +```julia +:(mutable struct Tadpole + _name::String + + Tadpole(_name::String) = new(_name) +end) +``` + +We see that the resulting expression is exactly what we would have expected, `Tadpole` is a mutable struct, it has one field called `_name` which is of type `String`. Also, `jluna` generate the default constructor, in case we need it later. This expression was evaluated when `implement()` was called, because types in julia cannot be run-time extended, we cannot change `Tadpole` at this point. If we try to, `jluna` will throw an `UsertypeAlreadyImplementedException`. Keep this in mind, `implement()` can only ever be called exactly once and after we did, we cannot chang the type further. + +Our tadpole is supposedly (un)boxable now so let's try it out: + +```cpp +// create cpp-side +auto cpp_tadpole = Frog::Tadpole(); + +// move to julia +State::new_named_undef("jl_tadpole") = cpp_tadpole; // equivalent to `= box(cpp_tadpole)` +jluna::safe_eval("println(jl_tadpole, " is of type ", typeof(jl_tadpole)")); + +// modify +jluna::safe_eval("jl_tadpole._name = \"Ted\""); + +// move back to cpp +cpp_tadpole = Main["jl_tadpole"]; // equivalent to unbox(Main["jl_tadpole"]) +std::cout << "tadpole is now named : " << cpp_tadpole.get_name() << std::endl; +``` +``` +Tadpole("") +tadpole is now named : Ted +``` +Everything works, we see that `jl_tadpole` is of type `Tadpole` and that when moving it from cpp to julia, the boxing routine was called and the empty name was preserved. After assigning the tadpoles name to `Ted`, when the unboxing routine was called during access through `operator[]` the name was again preserved and our cpp-side tadpole is now also named `Ted`. + +#### Finishing the Example + +You may have noticed that so far, we have neglected to implement `Frog::Tadpole::evolve`. This is because for that function to make sense, we first need to implement `Frog` itself. However `Frog` also has + +Now that we know the basics of this system, let us implement `Frog`. Recall that it looks like this C++-side: + +```cpp +class Frog +{ + private: + std::string _name; + + public: + struct Tadpole {/*...*/}; + + Frog(const std::string& name) + : _name + {} + + std::vector spawn(size_t number) + { + std::vector out; + for (size_t i = 0; i < number; ++i) + out.push_back(Tadpole()); + + return out; + } +}; +``` diff --git a/include/concepts.hpp b/include/concepts.hpp index 45e3aeb..cbd4bcb 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -12,6 +12,8 @@ #include #include +#include + namespace jluna { /// @concept: wrapper for std::is_same_v @@ -140,5 +142,11 @@ namespace jluna and not IsSet and not IsMap and not IsPair - and not IsTuple; + and not IsTuple + and not LambdaType + and not LambdaType + and not LambdaType + and not LambdaType + and not LambdaType + and not LambdaType>; } \ No newline at end of file From a757a6c6dd840fdf50030739ffa7583082194e03 Mon Sep 17 00:00:00 2001 From: clem Date: Wed, 2 Mar 2022 20:18:40 +0100 Subject: [PATCH 37/56] reworked interface, much nicer now --- .src/usertype.inl | 148 +++++++++---------------------- .test/main.cpp | 88 ++----------------- include/jluna.jl | 204 +++++++------------------------------------ include/usertype.hpp | 68 ++++----------- 4 files changed, 98 insertions(+), 410 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index e56f49e..8d7cadb 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -7,35 +7,21 @@ namespace jluna { - template - UsertypeNotFullyInitializedException::UsertypeNotFullyInitializedException() - { - std::stringstream str; - str << "\n[C++][EXCEPTION] Usertype interface for this type T has not yet been fully implemented, make sure the following are true:" << std::endl; - str << "\t1) T is default-constructible and a an implementation is available at compile time" << std::endl; - str << "\t2) Usertype::enable(\"\") was used to instance the usertype interface" << std::endl; - str << "\t3) Usertype::implement() was called, after which the interface cannot be extended" << std::endl; - str << "\nIf all of the above are true, T is now (un)boxable and to_julia_type::type_name is defined." << std::endl; - str << "(for more information, visit: https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes)" << std::endl; - _msg = str.str(); - } - - template - const char * UsertypeNotFullyInitializedException::what() const noexcept + namespace detail { - return _msg.c_str(); + struct to_julia_type_aux> + { + inline std::string type_name = ""; + }; } template - UsertypeAlreadyImplementedException::UsertypeAlreadyImplementedException() - { - std::stringstream str; - str << "[C++][EXCEPTION] Usertype::implement() was already called once for this type. It cannot be extended afterwards." << std::endl; - _msg = str.str(); - } + UsertypeNotEnabledException::UsertypeNotEnabledException() + : _msg("[C++][Exception] usertype interface for this type was not yet enabled. C Usertype::enable(const std::string&) to instance the interface") + {} template - const char * UsertypeAlreadyImplementedException::what() const noexcept + const char * UsertypeNotEnabledException::what() const noexcept { return _msg.c_str(); } @@ -43,69 +29,46 @@ namespace jluna template void Usertype::enable(const std::string& name) { - if (_implemented) - throw UsertypeAlreadyImplementedException(); - - jl_gc_pause; - static jl_function_t* new_usertype = jl_find_function("jluna.usertype", "new_usertype"); - _template = Proxy(jluna::safe_call(new_usertype, jl_symbol(name.c_str()))); + _enabled = true; + _name = std::make_unique(name); detail::to_julia_type_aux>::type_name = name; - jl_gc_unpause; - } - - template - std::string Usertype::get_name() - { - return _template["_name"].operator std::string(); } template - void Usertype::set_mutable(bool b) + bool Usertype::is_enabled() { - if (_implemented) - throw UsertypeAlreadyImplementedException(); - - _template["_is_mutable"] = b; + return _enabled; } template - bool Usertype::is_mutable() + template + void Usertype::add_property( + const std::string& name, + std::function box_get, + std::function unbox_set) { - return _template["_is_mutable"]; - } - - template - void Usertype::add_field(const std::string& name, const Type& type, std::function box_get, std::function unbox_set) - { - if (_implemented) - throw UsertypeAlreadyImplementedException(); - - jl_gc_pause; - static jl_function_t* add_field = jl_find_function("jluna.usertype", "add_field!"); - auto res = (*(_mapping.insert({name, {type, box_get, unbox_set}})).first); - jluna::safe_call(add_field, _template, jl_symbol(name.c_str()), std::get<0>(res.second).operator jl_datatype_t*()); - jl_gc_unpause; + _mapping.insert({Symbol(name), { + [box_get](T& instance) -> Any* {return jluna::box(box_get(instance));}, + [unbox_set](T& instance, Any* value) {unbox_set(instance, jluna::unbox(value));}, + Type(jl_eval_string(to_julia_type>::type_name)) + }}); } template Any* Usertype::box(T& in) { - if (not is_implemented()) - throw UsertypeNotFullyInitializedException(); + if (not _enabled) + throw UsertypeNotEnabledException(); jl_gc_pause; - static jl_function_t* set_field = jl_find_function("jluna.usertype", "set_field!"); + static jl_function_t* new_proxy = jl_find_function("jluna.cpp_proxy", "new_proxy"); + static jl_function_t* setfield = jl_get_function(jl_base_module, "setindex!"); + + Any* out = jluna::safe_call(new_proxy, _name.get()->operator _jl_sym_t *()); for (auto& pair : _mapping) - jluna::safe_call( - set_field, - _template, - jl_symbol(pair.first.c_str()), - std::get<1>(pair.second)(in) - ); - - auto* out = jl_call1(_implemented_type, _template); - forward_last_exception(); + jluna::safe_call(setfield, out, std::get<0>(pair.second)(in), pair.first); + jl_gc_unpause; return out; } @@ -113,47 +76,26 @@ namespace jluna template T Usertype::unbox(Any* in) { - if (not is_implemented()) - throw UsertypeNotFullyInitializedException(); + if (not _enabled) + throw UsertypeNotEnabledException(); jl_gc_pause; - static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); + static jl_function_t* getfield = jl_get_function(jl_base_module, "getindex"); auto out = T(); - for (auto pair : _mapping) - std::get<2>(pair.second)(out, jluna::safe_call(getfield, in, jl_symbol(pair.first.c_str()))); + for (auto& pair : _mapping) + std::get<1>(pair.second)(out, jluna::safe_call(getfield, in, pair.first)); jl_gc_unpause; return out; } - template - Type Usertype::implement(Module module) - { - if (_implemented) - throw UsertypeAlreadyImplementedException(); - - jl_gc_pause; - static jl_function_t* implement = jl_find_function("jluna.usertype", "implement"); - _implemented_type = jluna::safe_call(implement, _template, module); - _implemented = true; - jl_gc_unpause; - - return Type((jl_datatype_t*) _implemented_type); - } - - template - bool Usertype::is_implemented() - { - return _implemented; - } - template T unbox(Any* in) { - if (not Usertype::is_implemented()) - throw UsertypeNotFullyInitializedException(); + if (not Usertype::is_enabled()) + throw UsertypeNotEnabledException(); return Usertype::unbox(in); } @@ -161,19 +103,9 @@ namespace jluna template Any* box(T in) { - if (not Usertype::is_implemented()) - throw UsertypeNotFullyInitializedException(); + if (not Usertype::is_enabled()) + throw UsertypeNotEnabledException(); return Usertype::box(in); } - - namespace detail - { - template - struct to_julia_type_aux> - { - // usertype name is only available, after Usertype::Usertype(T) was called at least once - static inline std::string type_name = ""; - }; - } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 1a3a582..0290eea 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -66,88 +66,16 @@ int main() State::initialize(); Usertype::enable("Tadpole"); - Usertype::add_field( - "_name", // name of the field - String_t, // type of the field - // boxing routine - [](Frog::Tadpole& in) -> Any* - { - // box field value from cpp-tadpole and send to julia - return box(in.get_name()); - }, - // unboxing routine - [](Frog::Tadpole& out, Any* name_value) -> void - { - // unbox name from julia-tadpole assign cpp-tadpole instance - out.set_name(unbox(name_value)); - return; - } - ); - Usertype::add_field( - "evolve", - Function_t, - [](Frog::Tadpole& in) -> Any* - { - return box([](Any* tadpole_instance) -> Any* { - - Frog::Tadpole in = unbox(tadpole_instance); - return box(Frog(in.get_name())); - }); - }, - [](Frog::Tadpole& _, Any* __) -> void - { - return; - } - ); - - Usertype::enable("Frog"); - Usertype::set_mutable(false); - Usertype::add_field( - "name", - String_t, - [](Frog& in) -> Any* - { - return box(in.get_name()); - }, - [](Frog& out, Any* name) -> void - { - out = Frog(unbox(name)); - return; - } - ); - Usertype::add_field( - "spawn", - Function_t, - [](Frog& in) -> Any* - { - return box([in](Any* frog_instance, Any* number) -> Any* { - - Frog in = unbox(frog_instance); - size_t n = unbox(number); - - return box(in.spawn(n)); - }); - } + Usertype::add_property( + "_name", + [](Frog::Tadpole& instance) -> std::string {return instance.get_name();}, + [](Frog::Tadpole& instance, std::string name) {instance.set_name(name);} ); - Usertype::implement(); - Usertype::implement(); - - return 0; - - // create cpp-side -auto cpp_tadpole = Frog::Tadpole(); - -// move to julia -State::new_named_undef("jl_tadpole") = cpp_tadpole; // equivalent to `= box(cpp_tadpole)` -jluna::safe_eval("println(jl_tadpole)"); - -// modify -jluna::safe_eval("jl_tadpole._name = \"Ted\""); - -// move back to cpp -cpp_tadpole = Main["jl_tadpole"]; -std::cout << "tadpole is now named : " << cpp_tadpole.get_name() << std::endl; + auto cpp_pole = Frog::Tadpole(); + cpp_pole.set_name("Ted"); + State::new_named_undef("julia_pole") = box(cpp_pole); + State::eval("println(julia_pole)"); return 0; /* diff --git a/include/jluna.jl b/include/jluna.jl index 0a96e96..94ba980 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -958,191 +958,53 @@ module jluna end end - """ - interface for jluna::Usertype - """ - module usertype - - """ - usertype wrapper - """ - mutable struct Usertype - - _name::Symbol - _is_mutable::Bool - _field_types::Dict{Symbol, Type} - _field_values::Dict{Symbol, Any} - _parameters::Vector{TypeVar} - - Usertype(name::Symbol, is_mutable::Bool = true) = new( - name, - is_mutable, - Dict{Symbol, Type}(), - Dict{Symbol, Any}(), - Vector{TypeVar}() - ) - end - export Usertype - - """ - `new_usertype(::Symbol) -> Usertype` + module cpp_proxy - create new usertype - """ - function new_usertype(name::Symbol) ::Usertype - return Usertype(name) - end - export new_usertype - - """ - `set_mutable(::Usertype, ::Bool) -> Nothing` - - change mutability of usertype - """ - function set_mutable!(x::Usertype, value::Bool) ::Nothing - x._is_mutable = value - return nothing - end - export set_mutable! - - """ - `add_field!(::Usertype, name::Symbol, typename::Symbol) -> Nothing` - - add field to usertype, can also be a function - """ - function add_field!(x::Usertype, name::Symbol, type::Type) ::Nothing - - x._field_types[name] = type - x._field_values[name] = missing - return nothing - end - export add_field! - - """ - `set_field!(::Usertype, ::Symbol, value) -> Nothing` - - set value of field in usertype - """ - function set_field!(x::Usertype, name::Symbol, value) ::Nothing - - @assert haskey(x._field_values, name) - x._field_values[name] = value - return nothing; - end + const _proxy_id = Base.Ref{UInt64}(0x00000000ffffffff) - """ - `add_parameter!(::Usertype, ::Symbol, upper_bound::Type, lower_bound::Type) -> Nothing` + struct ProxyInternal - add parameter, including upper and lower bounds - """ - function add_parameter!(x::Usertype, name::Symbol, ub::Type = Any, lb::Type = Union{}) ::Nothing - push!(x._parameters, TypeVar(name, lb, ub)) - return nothing + _fields::Dict{Symbol, Union{Any, Missing}} + ProxyInternal() = new(Dict{Symbol, Union{Any, Missing}}()) end - export add_parameter! - - """ - `implement(::Usertype) -> Type` - evaluate the type - """ - function implement(type::Usertype, m::Module = Main) + struct Proxy - parameters = :() + _id::UInt64 + _typename::Symbol + _value::ProxyInternal - if isempty(type._parameters) - parameters = type._name - else - @assert false - parameters = Expr(:curly, type._name) - for tv in type._parameters - if tv.lb == Union{} - if tv.ub == Any - push!(param.args, tv.name) - else - push!(parameters.args, Expr(:(<:), tv.name, Symbol(tv.ub))) - end - else - push!(parameters.args, Expr(:comparison, Symbol(tv.lb), :(<:), tv.name, :(<:), Symbol(tv.ub))) - end - end + function Proxy(name::Symbol) + global _proxy_id.x += 1 + new(cpp_proxy._proxy_id.x, name, ProxyInternal()) end + end + export proxy - block = Expr(:block) - - for (field_name, field_type) in type._field_types - push!(block.args, Expr(:(::), field_name, (field_type isa Function ? :(Function) : Symbol(field_type)))) - end - - ctor::Expr = :() - default_ctor::Expr = :() - - if isempty(type._parameters) - ctor = Expr(:(=), :($(type._name)(base::Main.jluna.usertype.Usertype)), Expr(:call, :new)) - - default_ctor = Expr(:(=), Expr(:call, type._name), Expr(:call, :new)); - for (field_name, field_type) in type._field_types - push!(default_ctor.args[1].args, Expr(:(::), field_name, field_type)) - push!(default_ctor.args[2].args, field_name) - end - else - @assert false - curly_new = Expr(:curly, :new); - for t in type._parameters - push!(curly_new.args, t.name) - end - - where_call = Expr( - :where, - Expr( - :call, - Expr( - :curly, - type._name, - (collect(p.name for p in type._parameters)...) - ), - :(base::jluna.usertype.Usertype) - ), - (collect(p.name for p in type._parameters)...) - ) - - ctor = Expr(:(=), where_call, Expr(:call, curly_new)) - - where_call = Expr( - :where, - Expr( - :call, - Expr( - :curly, - type._name, - (collect(p.name for p in type._parameters)...) - ) - ), - (collect(p.name for p in type._parameters)...) - ) - - default_ctor = Expr(:(=), where_call, Expr(:call, curly_new, (collect(missing for _ in 1:length(type._parameters))...))) - end + new_proxy(name::Symbol) = return Proxy(name) + end +end - for (field_name, _) in type._field_values - field_symbol = QuoteNode(field_name) - push!(ctor.args[2].args, :(base._field_values[$(field_symbol)])) - end +""" +`setindex!(::Proxy, <:Any, ::Symbol) -> Nothing` - push!(block.args, ctor) - push!(block.args, default_ctor); +extend base.setindex! +""" +function Base.setindex!(proxy::Main.jluna.cpp_proxy.Proxy, value, key::Symbol) ::Nothing + proxy._value._fields[key] = value + return nothing +end +export setindex! - out::Expr = :(mutable struct $(parameters) end) - out.args[3] = block +""" +`getindex(::Proxy, ::Symbol) -> Any` - println(out) - Base.eval(m, out) - return getfield(m, type._name) - end - export implement - end - using Main.jluna.usertype +extend base.getindex +""" +function Base.getindex(proxy::Main.jluna.cpp_proxy.Proxy, value, key::Symbol) #::Auto + return proxy._value._fields[key] end +export getindex """ `cppall(::Symbol, ::Any...) -> Any` diff --git a/include/usertype.hpp b/include/usertype.hpp index ec4b817..1355f7b 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -23,22 +23,15 @@ namespace jluna /// @brief original type using original_type = T; - /// @brief enable this type by giving it a name - /// @param name: julia-side name - static void enable(const std::string& name); + Usertype() = delete; - /// @brief get julia-side name - /// @returns name - static std::string get_name(); + /// @brief enable interface + /// @param name + static void enable(const std::string&); - /// @brief set mutability, no by default - /// @param bool - /// @note this function will throw if called after implement() - static void set_mutable(bool); - - /// @brief get mutability + /// @brief is enabled /// @returns bool - static bool is_mutable(); + static bool is_enabled(); /// @brief add field /// @param name: julia-side name of field @@ -46,22 +39,13 @@ namespace jluna /// @param box_get: lambda with signature (T&) -> Any* /// @param unbox_set: lambda with signature (T&, Any*) /// @note this function will throw if called after implement() - static void add_field( + template + static void add_property( const std::string& name, - const Type& type, - std::function box_get, - std::function unbox_set = noop_set + std::function box_get, + std::function unbox_set = noop_set ); - /// @brief push to state and eval, cannot be extended afterwards - /// @param module: module the type will be set in - /// @returns julia-side type - static Type implement(Module module = Main); - - /// @brief is already implemented - /// @brief true if implement was called, false otherwise - static bool is_implemented(); - /// @brief box interface static Any* box(T&); @@ -69,41 +53,23 @@ namespace jluna static T unbox(Any*); private: - static inline Proxy _template = Proxy(jl_nothing); - - static inline bool _implemented = false; - static inline Any* _implemented_type = nullptr; + static inline bool _enabled = false; - static inline std::map _name = std::unique_ptr(nullptr); + static inline std::map, // getter - std::function // setter + std::function, // setter + Type >> _mapping = {}; }; /// @brief exception thrown when usertype is used before being implemented template - struct UsertypeNotFullyInitializedException : public std::exception - { - public: - /// @brief ctor - UsertypeNotFullyInitializedException(); - - /// @brief what - /// @returns message - const char* what() const noexcept override final; - - private: - std::string _msg; - }; - - /// @brief exception thrown implementation is called twice - template - struct UsertypeAlreadyImplementedException : public std::exception + struct UsertypeNotEnabledException : public std::exception { public: /// @brief ctor - UsertypeAlreadyImplementedException(); + UsertypeNotEnabledException(); /// @brief what /// @returns message From d9db01b200068a2163e83dc0ccd22eab6e2b4b1b Mon Sep 17 00:00:00 2001 From: clem Date: Thu, 3 Mar 2022 14:58:13 +0100 Subject: [PATCH 38/56] proxy working --- .src/usertype.inl | 16 +++++++--------- include/jluna.jl | 41 +++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 8d7cadb..da08eb6 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -7,13 +7,11 @@ namespace jluna { - namespace detail + template + struct to_julia_type> { - struct to_julia_type_aux> - { - inline std::string type_name = ""; - }; - } + inline static std::string type_name = ""; + }; template UsertypeNotEnabledException::UsertypeNotEnabledException() @@ -31,7 +29,7 @@ namespace jluna { _enabled = true; _name = std::make_unique(name); - detail::to_julia_type_aux>::type_name = name; + to_julia_type>::type_name = name; } template @@ -50,7 +48,7 @@ namespace jluna _mapping.insert({Symbol(name), { [box_get](T& instance) -> Any* {return jluna::box(box_get(instance));}, [unbox_set](T& instance, Any* value) {unbox_set(instance, jluna::unbox(value));}, - Type(jl_eval_string(to_julia_type>::type_name)) + Type((jl_datatype_t*) jl_eval_string(to_julia_type::type_name.c_str())) }}); } @@ -61,7 +59,7 @@ namespace jluna throw UsertypeNotEnabledException(); jl_gc_pause; - static jl_function_t* new_proxy = jl_find_function("jluna.cpp_proxy", "new_proxy"); + static jl_function_t* new_proxy = jl_find_function("jluna", "new_proxy"); static jl_function_t* setfield = jl_get_function(jl_base_module, "setindex!"); Any* out = jluna::safe_call(new_proxy, _name.get()->operator _jl_sym_t *()); diff --git a/include/jluna.jl b/include/jluna.jl index 94ba980..e682693 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -958,39 +958,40 @@ module jluna end end - module cpp_proxy + # internal id of proxies, starts at Max(Int32) to avoid id collision with UnnamedFunctionProxy + const _proxy_id = Base.Ref{UInt64}(0x00000000ffffffff) - const _proxy_id = Base.Ref{UInt64}(0x00000000ffffffff) + # obfuscate internal state to encourage using operator[] sytanx + struct ProxyInternal - struct ProxyInternal - - _fields::Dict{Symbol, Union{Any, Missing}} - ProxyInternal() = new(Dict{Symbol, Union{Any, Missing}}()) - end + _fields::Dict{Symbol, Union{Any, Missing}} + ProxyInternal() = new(Dict{Symbol, Union{Any, Missing}}()) + end - struct Proxy + # proxy as deepcopy of cpp-side usertype object + struct Proxy - _id::UInt64 - _typename::Symbol - _value::ProxyInternal + _id::UInt64 + _typename::Symbol + _value::ProxyInternal - function Proxy(name::Symbol) - global _proxy_id.x += 1 - new(cpp_proxy._proxy_id.x, name, ProxyInternal()) - end + function Proxy(name::Symbol) + global _proxy_id.x += 1 + new(_proxy_id.x, name, ProxyInternal()) end - export proxy - - new_proxy(name::Symbol) = return Proxy(name) end + export proxy + new_proxy(name::Symbol) = return Proxy(name) end +using Main.jluna; + """ `setindex!(::Proxy, <:Any, ::Symbol) -> Nothing` extend base.setindex! """ -function Base.setindex!(proxy::Main.jluna.cpp_proxy.Proxy, value, key::Symbol) ::Nothing +function Base.setindex!(proxy::Main.jluna.Proxy, value, key::Symbol) ::Nothing proxy._value._fields[key] = value return nothing end @@ -1001,7 +1002,7 @@ export setindex! extend base.getindex """ -function Base.getindex(proxy::Main.jluna.cpp_proxy.Proxy, value, key::Symbol) #::Auto +function Base.getindex(proxy::Main.jluna.Proxy, value, key::Symbol) #::Auto return proxy._value._fields[key] end export getindex From 6524b07a0dbcf75dbe81416d29d040be86b70bd2 Mon Sep 17 00:00:00 2001 From: clem Date: Thu, 3 Mar 2022 16:16:38 +0100 Subject: [PATCH 39/56] outline of usertype conversion manual done --- .gitignore | 1 + .test/main.cpp | 48 +++- docs/manual.md | 608 +++++++++++-------------------------------------- 3 files changed, 169 insertions(+), 488 deletions(-) diff --git a/.gitignore b/.gitignore index d6cd193..1c1010b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /libjluna_c_adapter.so /.src/include_julia.inl /.benchmark/results/ +/docs/bckp.txt diff --git a/.test/main.cpp b/.test/main.cpp index 0290eea..f0574ce 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,6 +11,21 @@ using namespace jluna; using namespace jluna::detail; +struct RGBA +{ + RGBA() + : _red(0), + _green(0), + _blue(0), + _alpha(1) + {} + + float _red; + float _green; + float _blue; + float _alpha; +}; + class Frog { private: @@ -65,17 +80,32 @@ int main() { State::initialize(); - Usertype::enable("Tadpole"); - Usertype::add_property( - "_name", - [](Frog::Tadpole& instance) -> std::string {return instance.get_name();}, - [](Frog::Tadpole& instance, std::string name) {instance.set_name(name);} + Usertype::enable("RGBA"); + Usertype::add_property( + "_red", + [](RGBA& in) -> float {return in._red;}, + [](RGBA& out, float in) -> void {out._red;} + ); + Usertype::add_property( + "_green", + [](RGBA& in) -> float {return in._green;}, + [](RGBA& out, float in) -> void {out._green;} ); + Usertype::add_property( + "_blue", + [](RGBA& in) -> float {return in._blue;}, + [](RGBA& out, float in) -> void {out._blue;} + ); + Usertype::add_property( + "_alpha", + [](RGBA& in) -> float {return in._alpha;}, + [](RGBA& out, float in) -> void {out._alpha;} + ); + + auto instance = RGBA(); + auto* res = box(instance); + - auto cpp_pole = Frog::Tadpole(); - cpp_pole.set_name("Ted"); - State::new_named_undef("julia_pole") = box(cpp_pole); - State::eval("println(julia_pole)"); return 0; /* diff --git a/docs/manual.md b/docs/manual.md index 79f5272..219fdb5 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1839,558 +1839,202 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes -### Example +So far, we were only able to box and unbox types that were already supported by `jluna`. While this list of types is broad, in more specialized applications it is sometimes necessary to define our own boxing/unboxing routines, such that we can (un)box arbitrary types. This section will give guidance on how to achieve this. -So far, we could only exchange information between the Julia and C++ state if its type was (un)boxable. This list of types while broad, is limited. This is why `jluna` offers an interface that allows users to specify their own (un)boxing routines and make *arbitrary* C++ types (un)boxable. We do this through a wrapper type `jluna::Usertype`. +### Usertype Interface -Unlike the previous sections, it may be best to illustrate the use of the usertype interface through an example. Let's say we want the following C++ class to be (un)boxable: +Consider the following C++ class: ```cpp -class Frog +struct RGBA { - private: - std::string _name; - - public: - struct Tadpole - { - Tadpole() = default; - - void set_name(const std::string& name) - { - _name = name; - } - - const std::string& get_name() const - { - return _name; - } - - Frog evolve() - { - assert(_name != ""); - return Frog(name); - } - - private: - std::string _name; - - }; - - Frog(const std::string& name) - : _name - {} - - std::vector spawn(size_t number) - { - std::vector out; - for (size_t i = 0; i < number; ++i) - out.push_back(Tadpole()); - - return out; - } + float _red; + float _green; + float _blue; + float _alpha; }; ``` -Let's first investigate this class. The outer class, `Frog`, only has a single field called `_name`. It has one ctor that takes the name and sets it, after constructing there is no way to renamed the frog. `Frog` other function is `spawn` which returns vector of `Frog::Tadpole`s. `Frog::Tadpole` is an internal class, tadpoles also have a name except they don't when they are born, we will have to name them some time afterwards. We can evolve a tadpole into a frog of the same name using `evolve`, however at that time, the tadpole has to have been named or an assertion is raised. +To make classes like this unboxable, we can use the `jluna::Usertype`, a usertype interface that provides a safe, easy way to transfer classes like this between C++ and Julia. -When translating this class in Julia, we run into a number of problems. There are no traditional internal classes in julia, if we define a struct inside another struct, both will be available in the outer structs namespace. Julia furthermore has no concept of visiblity, all its members are public. Lastly, julia does not have traditional member functions, we can have members that are functions but that functions definition does not have implicit access to the classes other members, as it would in C++. +#### Step 1: Making the Type compliant -### Utilizing Usertypes - -When attempting to transfer frogs and tadpoles between states, one might naively do the following: +For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no default constructor, so we need to add it: ```cpp -using namespace julia -State::initialize(); - -auto cpp_side_instance = Frog("Eve"); -State::new_named_undef("julia_side_instace") = box(cpp_side_instance); -``` - -This is actually valid syntax, it will compile. Executing the above, however, throws at runtime: - -``` -terminate called after throwing an instance of 'jluna::UsertypeNotFullyInitializedException' - what(): -[C++][EXCEPTION] Usertype interface for this type T has not yet been fully implemented, make sure the following are true: - 1) T is default-constructible and a an implementation is available at compile time - 2) Usertype::enable("") was used to instance the usertype interface - 3) Usertype::implement() was called, after which the interface cannot be extended - -If all of the above are true, T is now (un)boxable and to_julia_type::type_name is defined. +struct RGBA +{ + RGBA() + : _red(0), + _green(0), + _blue(0), + _alpha(1) + {} + + float _red; + float _green; + float _blue; + float _alpha; +}; ``` -This exception is very verbose and explains to us, what we need to do before a call like `box` can be successfull. Because explaining a process inside a single exception can make things quite dense, we'll go through the process one-by-one. - -### Usertype Interface - -To make an arbitrary type (un)boxable, we can implement its *usertype interface* `jluna::Usertype`. This class is a singleton, meaning there can only ever be one instance of it over the live of the program. Because we need `Tadpole` already (un)boxable to define `Frog`, let's start with the former +Furthermore, `T` (un)boxable via other functions in `jluna`, this means `T` cannot be a type mentioned in [this list of (un)boxables](#list-of-unboxables). -#### Step 1: Enabling the Interface +#### Step 2: Enabling the Interface -To instance the interface as a singleton, we use `Usertype::enable(const std::string&)`. This static function takes a name, this is not the name of the tadpole but the symbol of the type that we will be generating julia side. Most of the time we want the types name to be the same as the C++ class' name: +By default, the statement `box(/*...*/)` and `unbox(/*...*/)` compiles for any `T`. However if we try to do this with our `RGBA` class: ```cpp -// in main -Usertype::enable("Tadpole"); -Usertype::enable("Frog"); +auto instance = RGBA(); +auto* res = box(instance); ``` -Once we finished implementing these types, their will be two julia-side types `Frog` and `Tadpole`, because we gave them these names here. - -#### Step 2 (optional): Setting Mutability - -Next we will need to decide wether we want the julia-side `Frog` and `Tadpole` to be mutable or immutable struct types. Looking at our initial C++ classes, we notice that all member of tadpole, namely just `_name`, can be both read and written to via `set_name` and `get_name`. Because of this, its best not directly translate this behavior via two member function but rather just make the entirety of `Tadpole` mutable: - -```cpp -Usertype::set_mutable(true); ``` +terminate called after throwing an instance of 'jluna::UsertypeNotEnabledException' + what(): [C++][Exception] usertype interface for this type was not yet enabled. C Usertype::enable(const std::string&) to instance the interface -Frogs name cannot be changed because there is no setter, only a getter. This behavior is identical to an immutable struct type in julia so we should define it as such: +signal (6): Aborted +``` +An exception is thrown. To avoid unintended behavior, `jluna` requires users to manually enable types managed by `Usertype` by instancing the interface and naming them. We can do so using: ```cpp -Usertype::set_mutable(false); +Usertype::enable("RGBA"); ``` +The name specific here will be available through the julia-side object that is the result of the `box` call. We will learn how to access it soon. -This step is optional because by default, all usertypes are mutable. Usually, however, it is better style to manually declare them so. - -#### Step 3 (optional): Adding Fields & Methods - -To add fields to our julia types, we use `Usertype::add_field`. This class takes 4 arguments: +#### Step 3: Adding Property Routines -+ `const std::string& field_name`: name of the julia-side field -+ `Type& type`: type of the field -+ `std::function boxing_routine`: lambda with signature `(T&) -> Any*` that will be called during boxing. Its argument, of type `T&`, is the cpp-side instance of type `T` that we're trying to box. The result of this lambda will be assigned to the field `field_name` of the resulting julia-side type -+ `std::function unboxing_routine`: lambda with signature `(T&, Any*) -> void`, that is called during unboxing. The first argument `T&` is the result of the unboxing call. The second argument `Any*` is the current value of the julia-side field `field_name` +TODO: explain boxing and unboxing calls -Working through our example will be illustrative here. Firstly `Tadpole`: - -`Tadpole` has one field `_name` that is of type `std::string`. When boxing, the resulting julia-side tadpole should keep this name. Similarly, when unboxing, the now cpp-side tadpole should keep the name it had julia side. We can thus execute: +First we add a property for `_red`: ```cpp -Usertype::add_field( - "_name", // name of the field - String_t, // type of the field - // boxing routine - [](Frog::Tadpole& in) -> Any* - { - // box field value from cpp-tadpole and send to julia - return box(in.get_name()); - }, - // unboxing routine - [](Frog::Tadpole& out, Any* name_value) -> void - { - // unbox name from julia-tadpole assign cpp-tadpole instance - out.set_name(unbox(name_value)); - return; - } +Usertype::add_property( + "_red", + [](RGBA& in) -> float {return in._red;}, + [](RGBA& out float in) -> void {out._red;} ); ``` -#### Step 4: Implementing +Lets talk through this call one-by-one. -All that is left to do, concerning `Frog::Tadpole`, is to push our interface to julia using `Usertype::implement()`. This generates a new type of our specifications for us julia-side and enables (un)boxing for `Frog::Tadpole`: - -```cpp -Usertype::implement(); -``` +Firstly we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however after `RGBA` is moved to Julia, C++s `float` becomes Julias `Float32`. -This is not part of the actual release function but for illustration, let's see what jluna executes under-the-hood: +This first argument is the **name of the field**. It is best practice to have this be the same name as in C++, `_red` in this case, however there is no mechanism to enforced this. +The second argument is the **boxing routine**. This function is called during `box`. It has the signature `(T&) -> Value_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be unboxing like so: ```cpp -Usertype::enable("Tadpole"); -Usertype::set_mutable(true) -Usertype::add_field( - "_name", - String_t, - [](Frog::Tadpole& in) -> Any* - { - return box(in.get_name()); - }, - [](Frog::Tadpole& out, Any* name_value) -> void - { - out.set_name(unbox(name_value)); - return; - } -); -Usertype::implement(); -``` -```julia -:(mutable struct Tadpole - _name::String +auto instance = RGBA(); +Any* boxed = box(instance); - Tadpole(_name::String) = new(_name) -end) +// calls [](RGBA& in) -> float {return in._red;} +// with in = instance +// assign boxed._red with result of boxing routine ``` -We see that the resulting expression is exactly what we would have expected, `Tadpole` is a mutable struct, it has one field called `_name` which is of type `String`. Also, `jluna` generate the default constructor, in case we need it later. This expression was evaluated when `implement()` was called, because types in julia cannot be run-time extended, we cannot change `Tadpole` at this point. If we try to, `jluna` will throw an `UsertypeAlreadyImplementedException`. Keep this in mind, `implement()` can only ever be called exactly once and after we did, we cannot chang the type further. +The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type. -Our tadpole is supposedly (un)boxable now so let's try it out: +The third argument is the **unboxing routine**. This lambda has the signature `(T&, Value_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the resulting instance and the second argument is the value of the field of the corresponding name: ```cpp -// create cpp-side -auto cpp_tadpole = Frog::Tadpole(); +auto instance = RGBA(); +Any* boxed = box(instance); +RGBA unboxed = unbox(boxed); -// move to julia -State::new_named_undef("jl_tadpole") = cpp_tadpole; // equivalent to `= box(cpp_tadpole)` -jluna::safe_eval("println(jl_tadpole, " is of type ", typeof(jl_tadpole)")); - -// modify -jluna::safe_eval("jl_tadpole._name = \"Ted\""); - -// move back to cpp -cpp_tadpole = Main["jl_tadpole"]; // equivalent to unbox(Main["jl_tadpole"]) -std::cout << "tadpole is now named : " << cpp_tadpole.get_name() << std::endl; -``` +// calls [](RGBA& out, float in) +// where out = unboxed +// in = boxed._red ``` -Tadpole("") -tadpole is now named : Ted -``` -Everything works, we see that `jl_tadpole` is of type `Tadpole` and that when moving it from cpp to julia, the boxing routine was called and the empty name was preserved. After assigning the tadpoles name to `Ted`, when the unboxing routine was called during access through `operator[]` the name was again preserved and our cpp-side tadpole is now also named `Ted`. - -#### Finishing the Example -You may have noticed that so far, we have neglected to implement `Frog::Tadpole::evolve`. This is because for that function to make sense, we first need to implement `Frog` itself. However `Frog` also has +The third argument is optional, if no unboxing routine is specific, no operation happens during unboxing, `unboxed` is unmodified. -Now that we know the basics of this system, let us implement `Frog`. Recall that it looks like this C++-side: +Now that we know how to add fields, let's fully implement the usertype interface for `RGBA`. For completions sake, all previous stated code written here again. ```cpp -class Frog +struct RGBA { - private: - std::string _name; - - public: - struct Tadpole {/*...*/}; - - Frog(const std::string& name) - : _name - {} - - std::vector spawn(size_t number) - { - std::vector out; - for (size_t i = 0; i < number; ++i) - out.push_back(Tadpole()); - - return out; - } + float _red = 0; + float _green = 0; + float _blue = 0; + float _alpha = 1; + // is default constructable }; -``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#### Step 2: Adding Fields - -A type that is just name and nothing else is not very useful, thought. Instead we will need to give it fields that correspond to it's C++ fields. To do this, we use `Usertype::add_field`: - -This method takes 4 arguments: - -+ `const std::string& name`: name of the field julia-side -+ `Type&`: type of the julia-side field, can be `Any_t` -+ `std::function`: a lambda executed during box (see below) -+ `std::functionvoid`: a lambda executed during unbox (see below) - -#### Getter Lambda - -Both the fields name and type are self-explanatory, the lambdas are not. - -When a usertype T is **boxed**, each field added via `add_field` get sassigned a value. This value is the result of the first lambda expression with signature `(T&) -> Any*`, which we will call the **boxing routine** for that specific field. This lambda takes an instance of the type and returns a boxed value. Some examples: - -```cpp -// using a getter to acquire the value for the field -struct A - - std::string get_field() const - { - return _field; - } - - std::string set_field(std::string in) - { - _field = in; - } - - private: - std::string _field; -end - -Usertype::enable("A"); -Usertype::add_field("_field", String_t, [](A& instance) -> Any* { - // call getter on instance and return its value - return box(instance.get_field()); -}); -``` -In this example, during `Any* result_a = box(instance)`, the getter will be called to initialize the value of the field `_field` of `result_a` (which is of type `A`) to `instance.get_field()`. - -```cpp -// default initialize a field -struct B - Int64 _field = 1234; -end - -Usertype::enable("B"); -Usertype::add_field("_field", Int64_t, [](B& _) -> Any* { - // ignore instance and always return the same value - return box(1234); -}); -``` - -In this example, during `Any* result_b = box(instance)` the value of the field `_field` of `result_b` will always be `1234`, regardless of what value the instance holds. This can be thought of as default initialization, however the lambda still needs to conform to the signature `(T&) -> Any*`, which is why we name the argument `_` to signal that it is unused. - -```cpp -// computation unrelated to the instance to set the field -struct C - size_t id; -end - -static inline current_id = 33; - -Usertype::enable("C"); -Usertype::add_field("_id", UInt64_t, [&](B& _) -> Any* { - // capture global id and use it for initialize - current_id += 1; - return box(current_id); -}); +Usertype::enable("RGBA"); +Usertype::add_property( + "_red", + [](RGBA& in) -> float {return in._red;}, + [](RGBA& out, float in) -> void {out._red;} +); +Usertype::add_property( + "_green", + [](RGBA& in) -> float {return in._green;}, + [](RGBA& out, float in) -> void {out._green;} +); +Usertype::add_property( + "_blue", + [](RGBA& in) -> float {return in._blue;}, + [](RGBA& out, float in) -> void {out._blue;} +); +Usertype::add_property( + "_alpha", + [](RGBA& in) -> float {return in._alpha;}, + [](RGBA& out, float in) -> void {out._alpha;} +); ``` -Lastly, here we are using the lambda capture to access the value of `current_id`, which is unrelated to the instance. We increase this value of this global, then box it to hand to the result, such that for the expression `Any* result_c = box(instance)`, the value of the field `_id` will be what we handed it but we also modified a variable unrelated to the instance. - -What these three examples illustrate it similar to how boxing lambdas worked. While we are limited to the signature `(T&) -> Any*`, through captures we can actually execute arbitrary code. - -#### Setter Lambda - -Similar to how the `(T&) -> Any*` getter lambda governs, what the corresponding field of the result of the box expression will be set to, the **unbox routine** governs, what value the result calling `unbox(Any*)` will have. The unbox routine has the following signature: - -`(T& out, Any* field) -> T` - -Where `out` is the resulting object of type T and `field` is the julia-side value of the correponsing field of the julia-side type. Extending our previous examples: +To illustrate that the properties do not have to directly correspond with the C++ class, let's add another paremeter that represents the `value` component of the HSV color system, sometimes also called "lightness": ```cpp -// using a setter to modify the result -struct A - - std::string get_field() const - { - return _field; - } - - std::string set_field(std::string in) - { - _field = in; - } - - private: - std::string _field; -end - -Usertype::enable("A"); -Usertype::add_field( - "_field", // field name - String_t, // field type - // boxing routine - [](A& instance) -> Any* { - return box(instance.get_field()); - }, - // unboxing routine - [](A& instance, Any* field_value) -> void - { - instance.set_field(unbox(field_value)); - return +Usertype::add_property( + "_value", + [](RGBA& in) -> float { + float max = 0; + for (v : {in._red, in._green, in._blue}) + max = std::max(v, max); + return max; } ); ``` -Because `A`s `_field` is private, we can only modify it through `set_field`.`field_value`, here, is the value of the field name `_field`. After unboxing it, we can modify the resulting instance `instance` using the setter. +We leave the unboxing routine for `_value` unspecified, because there is no field to assign to C++-Side. -```cpp -// default initialize a field -struct B - Int64 _field = 1234; -end +#### Step 5: Implementing the Type -Usertype::enable("B"); -Usertype::add_field( - "_field", // field name - Int64_t, // field type - // boxing routine - [](B& _) -> Any* { - return box(1234); - }, - // unboxing routine - [](B& out, Any* field_value) -> void - { - out._field = unbox(field_value); - return; - } -); -``` -Here, we want the result of the unbox expression to acquire the value of the julia-side field. Because the member is public, we can simply assign it. +After fully specifying the usertype interface we now have to call `implement`: ```cpp -// computation unrelated to the instance to set the field -struct C - size_t id; -end - -static inline current_id = 33; -static inline number_of_cpp_side_instances = 12; - -Usertype::enable("C"); -Usertype::add_field( - "_id", // field name - UInt64_t, // field type - // boxing routine - [&](C& _) -> Any* - { - current_id += 1; - return box(current_id); - }, - // unboxing routine - [&](C& _, Any* _) -> void - - number_of_cpp_side_instnaces - - ); +Usertype::implement() ``` +This creates a new type of our specification usertype and sets up all behind-the-scene machinery, such that now, the following works: +```cpp +auto instance = RGBA(); +instance._red = 1; +instance._blue = 1; +State::new_named_undef("julia_side_instance") = box(instance); +jluna::safe_eval(R"( + println("julia_side_instance")); + julia_side_instance._blue = 0.5; +)") - - - - - - - - -Despite these differences, if we were to translate this C++ class into Julia, it would be something like: - -```julia -mutable struct Tadpole - - _name::String - evolve::Function - - Tadpole() = new("", - function (this::Tadpole) ::Frog - @assert this._name != "" - return Frog(this._name) - end - ) -end - -struct Frog - - _name::String - spawn::Function - - Frog(name::String) = new(name, - (this::Frog, number::Integer) -> [Tadpole() for _ in 1:number] - ) -end +auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instance")); +std::cout << cpp_side_instance._blue << std::endl; ``` - -Let's talk through why this is a faithful translation. Regarding `Tadpole`, in C++ we had a setter `set_name` and getter `get_name` for the property `_name`, this for the julia struct, we need to both be able to read and write to `_name`. By making `Tadpole` mutable, this is achieved. `Tadpole` is furthermore "default constructible" (in C++ parlance), that is, it has a constructor that takes no arguments and initializes the `_name` property as an empty string. Lastly we come to `Tadpole::evolve`, because julia doesn't have traditional member functions we employ the following design: - -```julia -# in cpp: -Frog Tadpole::evolve() -{ - assert((*this)._name != ""); - return Frog((*this)._name); -} - -#in julia -function Tadpole.evolve(this::Tadpole) ::Frog - @assert this._name != "" - return Frog(this._name) -end ``` -By making the classes instance an argument for the member function, we can simulate the behavior of a C++ member function. Calling both functions looks very similar: - -```julia -# in cpp -Tadpole instance = Tadpole(); -instance.evolve(); // equivalent to: instance.Tadpole::evolve() - -# in julia -instance::Tadpole = Tadpole(); -Tadpole.evolve(instance); +TODO ``` -Using this trick, we can simulate C++ syntax very closely. It is paramount to understand this, as it will be the basis for the rest of the section. - -Regarding the julia version of `Frog` now: In C++, the _name property can bread (via `get_name`) but cannot be set, we cannot renamed a frog after it evolved. To simulate this behavior, we make the julia-side frog an immutable struct. For the member function `spawn`, we utilize the same trick as with `Tadpole`: +Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`, such as assigning proxies and calling julia-side functions with C++-side arguments: -```julia -# in cpp -std::vector Frog::spawn(size_t number) -{ - std::vector out; - for (size_t i = 0; i < number; ++i) - out.push_back(Tadpole()); - - return out; -} - -# in julia -function Frog.spawn(number::Integer) ::Vector{Tadpole} - - out = Vector{Tadpole}() - for i in 1:number - push!(out, Tadpole()) - end - - return out -end +```cpp +RGBA proxy_conversion = Main["julia_side_instance"]; +Base["println"](cpp_side_instance); ``` - -Really, the only difference is that in julia, we have to define the member function during construction, rather than during struct declaration. - -### Usertype Interface - -Now that we know how to translate the classes, one might ask: how do we exchange memory from one state to another? Neither `Frog` nor `Tadpole` were listed in the number of types that are (un)boxable, so we will have to define our own unboxing routines, right? No, we do not. `jluna` understands that this requires an unreasonable amount of effort, due to how cumbersome the C-API is to use, so instead it gives us a centralized usertype interface: `Usertype`. - -For the interface to be able to handle a type T - ``` - - - +TODO +``` --- ## Performance @@ -2422,10 +2066,16 @@ Pair B Set B- Tuple C (unordered) map D -lambda D- +lambdas F + +Usertype ? ``` -Vectors and primitives can be directly swapped between language, which explains their excellent grade, while more complex objects need to be serialized or wrapped before being transferable. +Vectors and primitives can be directly swapped between language, which explains their excellent grade, while more complex objects need to be serialized or wrapped before being transferable. + +To make lambdas callable through julia, they have to through `jlunas` C-adapter library to then be wrapper Julia-side, which is too inefficient to be usable in performance critical code. However this only applies to boxing/unboxing the lambdas, usually we only need to box a lambda once, however after that, **calling already boxed lambdas is decently fast**. + +The performance of boxing/unboxing through the `Usertype` interface wholly depends on how optimal the user-defined getter/setters are implemented. Because of this, it is impossible to assign a grade. ### Minimize Proxy Construction From 53ce330167273a59bb743c835f34e1dcb564af77 Mon Sep 17 00:00:00 2001 From: clem Date: Thu, 3 Mar 2022 17:03:54 +0100 Subject: [PATCH 40/56] implement done --- .test/main.cpp | 19 ++++++++++++++++--- include/jluna.jl | 31 +++++++++++++++++++++++-------- include/usertype.hpp | 8 ++++++-- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index f0574ce..02ce2ec 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -101,11 +101,24 @@ int main() [](RGBA& in) -> float {return in._alpha;}, [](RGBA& out, float in) -> void {out._alpha;} ); + Usertype::add_property( + "_alpha", + [](RGBA& in) -> float {return in._alpha;}, + [](RGBA& out, float in) -> void {out._alpha;} + ); - auto instance = RGBA(); - auto* res = box(instance); - + Usertype::add_property( + "_value", + [](RGBA& in) -> float { + float max = 0; + for (float v : {in._red, in._green, in._blue}) + max = std::max(v, max); + return max; + } + ); + static jl_function_t* implement = jl_find_function("jluna", "implement"); + jluna::safe_call(implement, box(RGBA())); return 0; /* diff --git a/include/jluna.jl b/include/jluna.jl index e682693..9f9cc74 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -958,9 +958,6 @@ module jluna end end - # internal id of proxies, starts at Max(Int32) to avoid id collision with UnnamedFunctionProxy - const _proxy_id = Base.Ref{UInt64}(0x00000000ffffffff) - # obfuscate internal state to encourage using operator[] sytanx struct ProxyInternal @@ -971,17 +968,35 @@ module jluna # proxy as deepcopy of cpp-side usertype object struct Proxy - _id::UInt64 _typename::Symbol _value::ProxyInternal - function Proxy(name::Symbol) - global _proxy_id.x += 1 - new(_proxy_id.x, name, ProxyInternal()) - end + Proxy(name::Symbol) = new(name, ProxyInternal()) end export proxy new_proxy(name::Symbol) = return Proxy(name) + + function implement(template::Proxy, m::Module = Main) ::Type + + out::Expr = :(mutable struct $(template._typename) end) + deleteat!(out.args[3].args, 1) + + for (name, value) in template._value._fields + push!(out.args[3].args, Expr(:(::), name, Symbol(typeof(value)))) + end + + new_call::Expr = Expr(:(=), Expr(:call, template._typename), Expr(:call, :new)) + + for (_, value) in template._value._fields + push!(new_call.args[2].args, value) + end + + push!(out.args[3].args, new_call) + + Base.eval(m, out) + return m.eval(template._typename) + end + export implement end using Main.jluna; diff --git a/include/usertype.hpp b/include/usertype.hpp index 1355f7b..609cb24 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -17,7 +17,8 @@ namespace jluna template class Usertype { - static inline std::function noop_set = [](T&, Any* ) {return;}; + template + static inline std::function noop_set = [](T&, U) {return;}; public: /// @brief original type @@ -43,9 +44,12 @@ namespace jluna static void add_property( const std::string& name, std::function box_get, - std::function unbox_set = noop_set + std::function unbox_set = noop_set ); + /// + static void implement(Module module = Main); + /// @brief box interface static Any* box(T&); From 2a72789edbb92ca5edfe86659ae615efc3a7a830 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 4 Mar 2022 01:00:02 +0100 Subject: [PATCH 41/56] now preserves fieldname order --- .src/usertype.inl | 41 +++++++++++++++++++++++++++++++++++------ .test/main.cpp | 14 ++++++++++++-- include/jluna.jl | 16 +++++++++++----- include/usertype.hpp | 6 ++++++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index da08eb6..060b4e9 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -45,27 +45,53 @@ namespace jluna std::function box_get, std::function unbox_set) { - _mapping.insert({Symbol(name), { + auto symbol = Symbol(name); + + if (_mapping.find(name) == _mapping.end()) + _fieldnames_in_order.push_back(symbol); + + _mapping.insert({symbol, { [box_get](T& instance) -> Any* {return jluna::box(box_get(instance));}, [unbox_set](T& instance, Any* value) {unbox_set(instance, jluna::unbox(value));}, Type((jl_datatype_t*) jl_eval_string(to_julia_type::type_name.c_str())) }}); } + template + void Usertype::implement(Module module) + { + jl_gc_pause; + static jl_function_t* implement = jl_find_function("jluna", "implement"); + static jl_function_t* new_proxy = jl_find_function("jluna", "new_proxy"); + static jl_function_t* setfield = jl_get_function(jl_base_module, "setindex!"); + + auto default_instance = T(); + auto* template_proxy = jluna::safe_call(new_proxy, _name->operator Any*()); + + for (auto& field_name : _fieldnames_in_order) + jluna::safe_call(setfield, template_proxy, std::get<0>(_mapping.at(field_name))(default_instance), field_name); + + _type = std::make_unique((jl_datatype_t*) jluna::safe_call(implement, template_proxy)); + _implemented = true; + jl_gc_unpause; + } + template Any* Usertype::box(T& in) { if (not _enabled) throw UsertypeNotEnabledException(); + if (not _implemented) + implement(); + jl_gc_pause; - static jl_function_t* new_proxy = jl_find_function("jluna", "new_proxy"); - static jl_function_t* setfield = jl_get_function(jl_base_module, "setindex!"); + static jl_function_t* setfield = jl_get_function(jl_base_module, "setfield!"); - Any* out = jluna::safe_call(new_proxy, _name.get()->operator _jl_sym_t *()); + Any* out = jl_call0(_type->operator Any*()); for (auto& pair : _mapping) - jluna::safe_call(setfield, out, std::get<0>(pair.second)(in), pair.first); + jluna::safe_call(setfield, out, pair.first, std::get<0>(pair.second)(in)); jl_gc_unpause; return out; @@ -77,8 +103,11 @@ namespace jluna if (not _enabled) throw UsertypeNotEnabledException(); + if (not _implemented) + implement(); + jl_gc_pause; - static jl_function_t* getfield = jl_get_function(jl_base_module, "getindex"); + static jl_function_t* getfield = jl_get_function(jl_base_module, "getfield"); auto out = T(); diff --git a/.test/main.cpp b/.test/main.cpp index 02ce2ec..068eedc 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -117,8 +117,18 @@ int main() } ); - static jl_function_t* implement = jl_find_function("jluna", "implement"); - jluna::safe_call(implement, box(RGBA())); + auto cpp_side_instance = RGBA(); + cpp_side_instance._red = 0; + cpp_side_instance._blue = 0; + + State::new_named_undef("jl_side_instance") = cpp_side_instance; + jl_eval_string(R"( + jl_side_instance._blue = 0.5 + println(jl_side_instance) + )"); + + RGBA back = Main["jl_side_instance"]; + std::cout << back._blue << std::endl; return 0; /* diff --git a/include/jluna.jl b/include/jluna.jl index 9f9cc74..eb949de 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -961,8 +961,9 @@ module jluna # obfuscate internal state to encourage using operator[] sytanx struct ProxyInternal + _fieldnames_in_order::Vector{Symbol} _fields::Dict{Symbol, Union{Any, Missing}} - ProxyInternal() = new(Dict{Symbol, Union{Any, Missing}}()) + ProxyInternal() = new(Vector{Symbol}(), Dict{Symbol, Union{Any, Missing}}()) end # proxy as deepcopy of cpp-side usertype object @@ -981,14 +982,14 @@ module jluna out::Expr = :(mutable struct $(template._typename) end) deleteat!(out.args[3].args, 1) - for (name, value) in template._value._fields - push!(out.args[3].args, Expr(:(::), name, Symbol(typeof(value)))) + for name in template._value._fieldnames_in_order + push!(out.args[3].args, Expr(:(::), name, Symbol(typeof(template._value._fields[name])))) end new_call::Expr = Expr(:(=), Expr(:call, template._typename), Expr(:call, :new)) - for (_, value) in template._value._fields - push!(new_call.args[2].args, value) + for name in template._value._fieldnames_in_order + push!(new_call.args[2].args, template._value._fields[name]) end push!(out.args[3].args, new_call) @@ -1007,6 +1008,11 @@ using Main.jluna; extend base.setindex! """ function Base.setindex!(proxy::Main.jluna.Proxy, value, key::Symbol) ::Nothing + + if (!haskey(proxy._value._fields, key)) + push!(proxy._value._fieldnames_in_order, key) + end + proxy._value._fields[key] = value return nothing end diff --git a/include/usertype.hpp b/include/usertype.hpp index 609cb24..5dde4c9 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -50,6 +50,8 @@ namespace jluna /// static void implement(Module module = Main); + static bool is_implemented(); + /// @brief box interface static Any* box(T&); @@ -58,8 +60,12 @@ namespace jluna private: static inline bool _enabled = false; + static inline bool _implemented = false; + static inline std::unique_ptr _type = std::unique_ptr(nullptr); static inline std::unique_ptr _name = std::unique_ptr(nullptr); + + static inline std::vector _fieldnames_in_order = {}; static inline std::map, // getter std::function, // setter From 7d415a49588ba723e132cb1cc572434595a011e6 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 4 Mar 2022 01:04:51 +0100 Subject: [PATCH 42/56] working --- .src/usertype.inl | 8 ++++++-- .test/main.cpp | 15 ++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 060b4e9..4d7c049 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -51,8 +51,12 @@ namespace jluna _fieldnames_in_order.push_back(symbol); _mapping.insert({symbol, { - [box_get](T& instance) -> Any* {return jluna::box(box_get(instance));}, - [unbox_set](T& instance, Any* value) {unbox_set(instance, jluna::unbox(value));}, + [box_get](T& instance) -> Any* { + return jluna::box(box_get(instance)); + }, + [unbox_set](T& instance, Any* value) -> void { + unbox_set(instance, jluna::unbox(value)); + }, Type((jl_datatype_t*) jl_eval_string(to_julia_type::type_name.c_str())) }}); } diff --git a/.test/main.cpp b/.test/main.cpp index 068eedc..f51cb3d 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -84,27 +84,22 @@ int main() Usertype::add_property( "_red", [](RGBA& in) -> float {return in._red;}, - [](RGBA& out, float in) -> void {out._red;} + [](RGBA& out, float in) -> void {out._red = in;} ); Usertype::add_property( "_green", [](RGBA& in) -> float {return in._green;}, - [](RGBA& out, float in) -> void {out._green;} + [](RGBA& out, float in) -> void {out._green = in;} ); Usertype::add_property( "_blue", [](RGBA& in) -> float {return in._blue;}, - [](RGBA& out, float in) -> void {out._blue;} + [](RGBA& out, float in) -> void {out._blue = in;} ); Usertype::add_property( "_alpha", [](RGBA& in) -> float {return in._alpha;}, - [](RGBA& out, float in) -> void {out._alpha;} - ); - Usertype::add_property( - "_alpha", - [](RGBA& in) -> float {return in._alpha;}, - [](RGBA& out, float in) -> void {out._alpha;} + [](RGBA& out, float in) -> void {out._alpha = in;} ); Usertype::add_property( @@ -117,6 +112,8 @@ int main() } ); + Usertype::implement(); + auto cpp_side_instance = RGBA(); cpp_side_instance._red = 0; cpp_side_instance._blue = 0; From cb64a72fc790ec4f583679702ae46107fb4232cf Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 4 Mar 2022 01:30:38 +0100 Subject: [PATCH 43/56] updated readme --- .test/main.cpp | 37 +++++++++++++++++++++++++++++++++ README.md | 53 ++++++++++++++++++++++++++++++++++++++++++------ include/jluna.jl | 2 +- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index f51cb3d..c997c27 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -80,6 +80,43 @@ int main() { State::initialize(); + struct NonJuliaType +{ + Int64 _field01 = 123; + std::vector _field02; +}; + +Usertype::enable("NonJuliaType"); +Usertype::add_property( + // fieldname + "_field01", + // getter + [](NonJuliaType& in) -> Int64 {return in._field01;}, + // setter + [](NonJuliaType& out, Int64 value) {out._field01 = value;} +); + +Usertype::add_property>( + "_field02", + [](NonJuliaType& in) -> std::vector {return in._field02;}, + [](NonJuliaType& in, std::vector value) {in._field02 = value;} + +); + +Usertype::implement(); + +auto cpp_instance = NonJuliaType(); +State::new_named_undef("julia_instance") = cpp_instance; +// now exchangable between states! + +jluna::safe_eval(R"( + println(fieldnames(typeof(julia_instance))) + push!(julia_instance._field02, "new") + println(julia_instance) +)"); + +return 0; + Usertype::enable("RGBA"); Usertype::add_property( "_red", diff --git a/README.md b/README.md index 6d1fefe..4396412 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,49 @@ State::safe_script(R"( cpp prints what julia handed it and returns: 10 ``` +--- +#### Exchange Arbitrary Types between States + +```cpp +struct NonJuliaType +{ + Int64 _field01 = 123; + std::vector _field02; +}; + +// setup usertype interface +Usertype::enable("NonJuliaType"); +Usertype::add_property( + // fieldname + "_field01", + // getter + [](NonJuliaType& in) -> Int64 {return in._field01;}, + // setter + [](NonJuliaType& out, Int64 value) {out._field01 = value;} +); + +Usertype::add_property>( + "_field02", + [](NonJuliaType& in) -> std::vector {return in._field02;}, + [](NonJuliaType& in, std::vector value) {in._field02 = value;} +); + +// create julia-side equivalent type +Usertype::implement(); + +// can now be moved between Julia and C++ +auto cpp_instance = NonJuliaType(); +State::new_named_undef("julia_instance") = box(cpp_instance); + +jluna::safe_eval(R"( + push!(julia_instance._field02, "new") + println(julia_instance) +)"); +``` +``` +NonJuliaType(123, ["new"]) +``` + --- ### Features @@ -115,13 +158,12 @@ Some of the many advantages `jluna` has over the C-API include: + call C++ functions from julia using any julia-type + assigning C++-side proxies also mutates the corresponding variable with the same name julia-side + julia-side values, including temporaries, are kept safe from the garbage collector -+ verbose exception forwarding, compile-time assertions -+ wraps [most](./docs/manual.md#list-of-unboxables) of the relevant C++ `std` objects and types ++ any C++ type can be moved between Julia and C++ + multi-dimensional, iterable array interface with julia-style indexing -+ deep, C++-side introspection functionalities for julia objects ++ C++-side introspection that is deeper than what is possible through only Julia + fast! All code is considered performance-critical and was optimized for minimal overhead compared to the C-API -+ verbose manual, written by a human -+ inline documentation for IDEs for both C++ and Julia code ++ verbose exception forwarding, compile-time assertions ++ inline documentation for IDEs for both C++ and Julia code+ verbose manual, written by a human + freely mix `jluna` and the C-API + And more! @@ -129,7 +171,6 @@ Some of the many advantages `jluna` has over the C-API include: (in order of priority, highest first) -+ usertypes, creating modules and struct completely C++-side + thread-safety, parallelization + linear algebra wrapper, matrices + expression proxies diff --git a/include/jluna.jl b/include/jluna.jl index eb949de..68e8207 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -983,7 +983,7 @@ module jluna deleteat!(out.args[3].args, 1) for name in template._value._fieldnames_in_order - push!(out.args[3].args, Expr(:(::), name, Symbol(typeof(template._value._fields[name])))) + push!(out.args[3].args, Expr(:(::), name, :($(typeof(template._value._fields[name]))))) end new_call::Expr = Expr(:(=), Expr(:call, template._typename), Expr(:call, :new)) From b3bf09aee14b8a4d9fdd48682921e339349b051f Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 4 Mar 2022 19:40:38 +0100 Subject: [PATCH 44/56] stashing manual --- .test/main.cpp | 60 +++--------- README.md | 3 +- docs/manual.md | 236 ++++++++++++++++++++++++++++++++++++++++++----- include/jluna.jl | 2 +- 4 files changed, 229 insertions(+), 72 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index c997c27..609efe1 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -79,44 +79,7 @@ using namespace jluna; int main() { State::initialize(); - - struct NonJuliaType -{ - Int64 _field01 = 123; - std::vector _field02; -}; - -Usertype::enable("NonJuliaType"); -Usertype::add_property( - // fieldname - "_field01", - // getter - [](NonJuliaType& in) -> Int64 {return in._field01;}, - // setter - [](NonJuliaType& out, Int64 value) {out._field01 = value;} -); - -Usertype::add_property>( - "_field02", - [](NonJuliaType& in) -> std::vector {return in._field02;}, - [](NonJuliaType& in, std::vector value) {in._field02 = value;} - -); - -Usertype::implement(); - -auto cpp_instance = NonJuliaType(); -State::new_named_undef("julia_instance") = cpp_instance; -// now exchangable between states! - -jluna::safe_eval(R"( - println(fieldnames(typeof(julia_instance))) - push!(julia_instance._field02, "new") - println(julia_instance) -)"); - -return 0; - + /* Usertype::enable("RGBA"); Usertype::add_property( "_red", @@ -150,19 +113,20 @@ return 0; ); Usertype::implement(); + */ - auto cpp_side_instance = RGBA(); - cpp_side_instance._red = 0; - cpp_side_instance._blue = 0; + auto instance = RGBA(); +instance._red = 1; +instance._blue = 1; - State::new_named_undef("jl_side_instance") = cpp_side_instance; - jl_eval_string(R"( - jl_side_instance._blue = 0.5 - println(jl_side_instance) - )"); +State::new_named_undef("julia_side_instance") = box(instance); +jluna::safe_eval(R"( + println(julia_side_instance) + julia_side_instance._blue = 0.5; +)"); - RGBA back = Main["jl_side_instance"]; - std::cout << back._blue << std::endl; +auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instance")); +std::cout << cpp_side_instance._blue << std::endl; return 0; /* diff --git a/README.md b/README.md index 4396412..d9c681f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# jluna: A modern julia â­¤ C++ Wrapper (v0.7.0) +# jluna: A modern julia â­¤ C++ Wrapper (v0.8.0) ![](./header.png) @@ -148,7 +148,6 @@ jluna::safe_eval(R"( ``` NonJuliaType(123, ["new"]) ``` - --- ### Features diff --git a/docs/manual.md b/docs/manual.md index 219fdb5..f702347 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1878,7 +1878,7 @@ struct RGBA }; ``` -Furthermore, `T` (un)boxable via other functions in `jluna`, this means `T` cannot be a type mentioned in [this list of (un)boxables](#list-of-unboxables). +Furthermore, `T` cannot be (un)boxable via other functions in `jluna`. This means `T` can't be any type in [this list of (un)boxables](#list-of-unboxables). #### Step 2: Enabling the Interface @@ -1894,18 +1894,18 @@ terminate called after throwing an instance of 'jluna::UsertypeNotEnabledExcepti signal (6): Aborted ``` -An exception is thrown. To avoid unintended behavior, `jluna` requires users to manually enable types managed by `Usertype` by instancing the interface and naming them. We can do so using: +An exception is thrown at runtime. To avoid unintended behavior, `jluna` requires users to manually enable the usertype interface for any specific type. We can do so using: ```cpp Usertype::enable("RGBA"); ``` -The name specific here will be available through the julia-side object that is the result of the `box` call. We will learn how to access it soon. +This functions takes the julia-side name as an argument. After unboxing, this will be the name of the type, `RGBA` will be converted to. #### Step 3: Adding Property Routines -TODO: explain boxing and unboxing calls +A *property* of a struct type would be called a "field" in Julia and a "member" in C++. We will use all 3 terms interchangeably here. -First we add a property for `_red`: +Explaining how to add properties is best done via an example. Thus, we add a property for `_red`: ```cpp Usertype::add_property( @@ -1917,11 +1917,11 @@ Usertype::add_property( Lets talk through this call one-by-one. -Firstly we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however after `RGBA` is moved to Julia, C++s `float` becomes Julias `Float32`. +First, we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however after `RGBA` is moved to Julia, C++s `float` becomes Julias `Float32`. We can check which C++ types gets converted to which Julia type using `to_julia_type::type_name`. This first argument is the **name of the field**. It is best practice to have this be the same name as in C++, `_red` in this case, however there is no mechanism to enforced this. -The second argument is the **boxing routine**. This function is called during `box`. It has the signature `(T&) -> Value_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be unboxing like so: +The second argument is the **boxing routine**. This function is called during `box(instance)`. It has the signature `(T&) -> Value_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be boxed: ```cpp auto instance = RGBA(); @@ -1934,7 +1934,7 @@ Any* boxed = box(instance); The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type. -The third argument is the **unboxing routine**. This lambda has the signature `(T&, Value_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the resulting instance and the second argument is the value of the field of the corresponding name: +The third argument is the **unboxing routine**. This lambda has the signature `(T&, Value_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the resulting cpp-side instance, the second argument is the value of the field of the corresponding name: ```cpp auto instance = RGBA(); @@ -1946,9 +1946,9 @@ RGBA unboxed = unbox(boxed); // in = boxed._red ``` -The third argument is optional, if no unboxing routine is specific, no operation happens during unboxing, `unboxed` is unmodified. +The third argument is optional, if no unboxing routine is unspecified, `unboxed` is not modified. -Now that we know how to add fields, let's fully implement the usertype interface for `RGBA`. For completions sake, all previous stated code written here again. +Now that we know how to add fields, let's fully implement the usertype interface for `RGBA`. For completions sake, all previous code is reprinted here: ```cpp struct RGBA @@ -1982,7 +1982,7 @@ Usertype::add_property( [](RGBA& out, float in) -> void {out._alpha;} ); ``` -To illustrate that the properties do not have to directly correspond with the C++ class, let's add another paremeter that represents the `value` component of the HSV color system, sometimes also called "lightness": +To illustrate that the properties do not have to directly correspond with the C++ class, let's add another paremeter that represents the `value` component of the HSV color system, sometimes also called "lightness", is defined as the maximum of red, green and blue: ```cpp Usertype::add_property( @@ -1996,17 +1996,32 @@ Usertype::add_property( ); ``` -We leave the unboxing routine for `_value` unspecified, because there is no field to assign to C++-Side. +We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to. C++-Side. #### Step 5: Implementing the Type -After fully specifying the usertype interface we now have to call `implement`: +Having added all properties to the usertype interface, the only thing left to do is call: ```cpp Usertype::implement() ``` -This creates a new type of our specification usertype and sets up all behind-the-scene machinery, such that now, the following works: +This creates a new type julia-side that has the same architecture we gave it. For end-users this happens behind the scene, but for illustrations sake, this expression is assembled an evaluated during `implement`: + +```julia +mutable struct RGBA + _red::Float32 + _green::Float32 + _blue::Float32 + _alpha::Float32 + _value::Float32 + RGBA() = new(0.0f0, 0.0f0, 0.0f0, 1.0f0, 0.0f0) +end +``` + +We see that `jluna` assembled a struct type whos field names and types are as we have specified. The type is also `mutable` and it has a default construct (a constructor that takes no arguments). The default values here are taken from an unmodified instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. + +After having evaluated this expression and thus defining the type, the following works: ```cpp auto instance = RGBA(); @@ -2015,26 +2030,205 @@ instance._blue = 1; State::new_named_undef("julia_side_instance") = box(instance); jluna::safe_eval(R"( - println("julia_side_instance")); + println(julia_side_instance) julia_side_instance._blue = 0.5; -)") +)"); auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instance")); std::cout << cpp_side_instance._blue << std::endl; ``` ``` -TODO +RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) +0.5 ``` -Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`, such as assigning proxies and calling julia-side functions with C++-side arguments: +Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`. This includes assigning proxies, calling julia-side functions with C++-side arguments and even using `RGBA` as value types for a `jluna` array. + +### Manual Conversion + +> **Danger Zone**: This section is only intended for advanced users as it interacts with the raw C API. Caution and an expectancy of segfaults is advised. + +`jluna::Usertype` is pretty and easy to use, but it also takes some freedom away from the users. The resulting type is always a struct type, it is always mutable and we do not have fine-control over the entire boxing/unboxing process. Because of this, it is sometimes necessary to go "hands-on" and properly extend the `jluna` library. + +As a culmination of all the things we learned in this manual, we'll work through an example. Consider the following C++ classes: ```cpp -RGBA proxy_conversion = Main["julia_side_instance"]; -Base["println"](cpp_side_instance); +class Frog +{ + private: + std::string _name; + + public: + struct Tadpole + { + Tadpole() = default; + + void set_name(const std::string& name) + { + _name = name; + } + + const std::string& get_name() const + { + return _name; + } + + Frog evolve() + { + assert(_name != ""); + return Frog(name); + } + + private: + std::string _name; + + }; + + Frog(const std::string& name) + : _name + {} + + std::vector spawn(size_t number) + { + std::vector out; + for (size_t i = 0; i < number; ++i) + out.push_back(Tadpole()); + + return out; + } +}; ``` + +We have `Frog` and `Frog::Tadpole` (henceforth just `Tadpole`). A tadpole has one field `_name`. When constructing a `Tadpole`, it does not have a name, we need to name it afterwards using `set_name`. If a tadpole was named, we can call `evolve` which returns a `Frog` instance with the same name as that tadpole. + +`Frog`s, like `Tadpole`s, have a name. However, when a `Frog` is constructed, it cannot be unnamed and its name cannot be changed. `Frog`s have a member function to spawn a number of tadpoles, all unnamed, stored in a vector. + +While this example is fairly simple conceptually, it provides a number of problems in the context of a Julia-C++ application. Firstly, `Tadpole` depends on `Frog` being defined (as it is the return type of `:evole`) and `Frog` depends on `Tadpole` being defined. While this is perfectly valid in a static context, if we were to add usertypes one-by-one through `jluna`, this may cause problems. + +Secondly, translating the exact functionality to Julia isn't trivial. Julia doesn't have internal structs (a struct definition inside another struct is evaluated in the same namespace as the outer struct), and julia structtypes do not have member functions in the traditional, C++ sense. While a field of a Julia structtype can be a function, it does not have implicit access to the other fields of that type. + +Given these limitations, we decide to translate `Frog` and `Tadpole` "ourself", that is without going through the usertype interface. This section will guide users potentially looking at much more complex examples through this process. + +#### Defining the Julia-Side Types + +Independent of `jluna` or C++, we create two structs, also name `Frog` and `Tadpole` that attempt to translate the functionality as closely as possible. Whether this is the right approach is immaterial to this exercise. + +Tadpole has a setter and getter for its property `_name`. Because this is the only field, we can declared Tadpole `mutable`: + +```Julia +mutable struct Tadpole + _name::String +end ``` -TODO + +Next we add the "member function" `evolve`. A good paradigm to use for situations like this is the following: + +```Julia +# C++: +auto instance = InstanceType(); +instance.member_function(arguments); + +# Julia: +instance = InstanceType() +InstanceType.member_function(instance, arguments) +``` + +By making the `this` available through an argument, the member function has access to all the same things the C++ member function would have. Applying this design pattern to `Tadpole::evolve`: + +```Julia +function evolve(instance::Tadpole) + return Frog(instance._name) +end + +mutable struct Tadpole + _name::String +end +``` + +Because we want `evolve` to only be available through `Tadpole`, we can make it a member and add it to the constructor: + +```julia +mutable struct Tadpole + _name::String + evolve::Function + + Tadpole() = new( + "", + (instance::Tadpole) -> return Frog(instance._name) + ) +end +``` + +Where we used the `->` syntax to make `evolve` and anonymous function that binds to `Tadpole.evolve`. + +Turning our attention to `Frog`, now, we note that `Frog` only has a single field `_name` that only provides a getter, no setter. This behavior can be best emulated in julia using an immutable struct: + +```julia +struct Frog + _name::String +end +``` + +We add the constructor and `spawn` function: + +```julia +struct Frog + _name::String + spawn::Function + + Frog(name::String) = new( + name, + (number::Int64) -> return [Tadpole() for _ in 1:number] + ) +end +``` + +Where we used a generator expression to create a vector of length `number`, that we fill with freshly constructed tadpoles. + +#### Defining Box/Unbox + +Now that we have our Julia-side Tadpole and Frog, we need to connect them to C++ in some way. As stated before, the only thing that needs to be implemented for any C++ object to be transferable to julia is `box` and `unbox`. + +All box/unbox calls have to adhere to the following signatures: + +```cpp +template +Any* box(T); + +template +T box(Any*); ``` + +Experienced C++ users may notice that the latter would be ambigous if we were to define it for more than one `T`, because in C++, a functions signature can only be differentiated using the arguments, not the return type. To resolve this, we use [SFINAE](https://en.cppreference.com/w/cpp/language/sfinae) and [concepts](https://en.cppreference.com/w/cpp/language/constraints). Filling it out for `Frog`: + +```cpp +// using SFINAE: +template, Bool> = true> +Any* box(T); + +template, Bool> = true> +T unbox(Any*); + +// using Concepts: +template T> +Any* box(T); + +template T> +T unbox(Any*); +``` + +Where the latter is obviously much nicer syntax and thus preferred. + +TODO + + + + + + + + + --- ## Performance diff --git a/include/jluna.jl b/include/jluna.jl index 68e8207..db756f5 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -993,7 +993,7 @@ module jluna end push!(out.args[3].args, new_call) - + println(out) Base.eval(m, out) return m.eval(template._typename) end From 9e1b273d3b71a8e84428e7ba675740a4082d09d8 Mon Sep 17 00:00:00 2001 From: clem Date: Fri, 4 Mar 2022 19:53:04 +0100 Subject: [PATCH 45/56] stashing --- .test/main.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index 609efe1..fdd8922 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -129,7 +129,8 @@ auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instanc std::cout << cpp_side_instance._blue << std::endl; return 0; - /* + */ + Test::initialize(); Test::test("catch c exception", [](){ @@ -1202,7 +1203,22 @@ std::cout << cpp_side_instance._blue << std::endl; i += 1; } }); - */ + + struct NonJuliaType + { + std::vector _field; + }; + + Test::test("Usertype: throw on disable", [](){ + + Test::assert_that_throws([](){ + volatile box(NonJuliaType()); + }); + + Test::assert_that_throws([](){ + volatile unbox(jl_nothing); + }); + }); Test::conclude(); } From b1c0c43c8a9784b75bd0aaaa2d2b1816898017b5 Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 5 Mar 2022 01:22:26 +0100 Subject: [PATCH 46/56] first draft done --- docs/manual.md | 306 +++++++++++++------------------------------ include/state.hpp | 1 - include/usertype.hpp | 57 +++++++- 3 files changed, 149 insertions(+), 215 deletions(-) diff --git a/docs/manual.md b/docs/manual.md index f702347..651b560 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -40,7 +40,7 @@ Please navigate to the appropriate section by clicking the links below: 8.5 [Allowed Function Names](#allowed-function-names)
8.6 [Allowed Function Signatures](#possible-signatures)
8.7 [Using arbitrary Objects in julia Functions](#using-non-julia-objects-in-functions)
-9. [**Arrays**](#arrays)
+9. [**Specialized Proxies: Arrays**](#arrays)
9.1 [Constructing Arrays](#ctors)
9.2 [Indexing](#indexing)
9.3 [Iterating](#iterating)
@@ -57,12 +57,20 @@ Please navigate to the appropriate section by clicking the links below: 10.7 [~~Type Info: Properties~~](#properties)
10.8 [Type Classification](#type-classification)
11. [~~Expressions~~](#expressions)
-12. [~~Usertypes~~](#usertypes)
-13. [**Performance**](#performance)
- 13.1 [Staying Julia-Side](#stay-julia-side)
- 13.2 [(Un)Boxing](#minimize-unboxing)
- 13.3 [Proxy Construction](#minimize-proxy-construction)
- 13.4 [Using the C-Library](#use-the-c-library)
+12. [Usertypes](#usertypes)
+ 12.1 [Usertype Interface](#usertype-interface)
+ 12.2 [Making the Type Compliant](#step-1-making-the-type-compliant)
+ 12.3 [Enabling the Interface](#step-2-enabling-the-interface)
+ 12.4 [Adding Property Routines](#step-3-adding-property-routines)
+ 12.5 [Implementing the Type](#step-4-implementing-the-type)
+13. [~~**Paralell Execution~~](#performance)
+14. [**Performance**](#performance)
+ 14.1 [Cheat Sheet](#cheat-sheet)
+ 14.2 [Avoiding String Parsing](#avoid-string-parsing)
+ 14.3 [Staying Julia-Side](#stay-julia-side)
+ 14.4 [(Un)Boxing](#minimize-unboxing)
+ 14.5 [Proxy Construction](#minimize-proxy-construction)
+ 14.6 [Using the C-Library](#use-the-c-library)
## Initialization @@ -1998,7 +2006,7 @@ Usertype::add_property( We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to. C++-Side. -#### Step 5: Implementing the Type +#### Step 4: Implementing the Type Having added all properties to the usertype interface, the only thing left to do is call: @@ -2019,9 +2027,9 @@ mutable struct RGBA end ``` -We see that `jluna` assembled a struct type whos field names and types are as we have specified. The type is also `mutable` and it has a default construct (a constructor that takes no arguments). The default values here are taken from an unmodified instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. +We see that `jluna` assembled a struct type whos field names and types are as we have specified. The type is also `mutable` and it has a default construct (a constructor that takes no arguments). The default values here are taken from an unmodified instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. -After having evaluated this expression and thus defining the type, the following works: +After having now evaluated this expression and defining the type, the following works: ```cpp auto instance = RGBA(); @@ -2044,198 +2052,95 @@ RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`. This includes assigning proxies, calling julia-side functions with C++-side arguments and even using `RGBA` as value types for a `jluna` array. -### Manual Conversion - -> **Danger Zone**: This section is only intended for advanced users as it interacts with the raw C API. Caution and an expectancy of segfaults is advised. - -`jluna::Usertype` is pretty and easy to use, but it also takes some freedom away from the users. The resulting type is always a struct type, it is always mutable and we do not have fine-control over the entire boxing/unboxing process. Because of this, it is sometimes necessary to go "hands-on" and properly extend the `jluna` library. - -As a culmination of all the things we learned in this manual, we'll work through an example. Consider the following C++ classes: - -```cpp -class Frog -{ - private: - std::string _name; - - public: - struct Tadpole - { - Tadpole() = default; - - void set_name(const std::string& name) - { - _name = name; - } - - const std::string& get_name() const - { - return _name; - } - - Frog evolve() - { - assert(_name != ""); - return Frog(name); - } - - private: - std::string _name; - - }; - - Frog(const std::string& name) - : _name - {} - - std::vector spawn(size_t number) - { - std::vector out; - for (size_t i = 0; i < number; ++i) - out.push_back(Tadpole()); - - return out; - } -}; -``` - -We have `Frog` and `Frog::Tadpole` (henceforth just `Tadpole`). A tadpole has one field `_name`. When constructing a `Tadpole`, it does not have a name, we need to name it afterwards using `set_name`. If a tadpole was named, we can call `evolve` which returns a `Frog` instance with the same name as that tadpole. - -`Frog`s, like `Tadpole`s, have a name. However, when a `Frog` is constructed, it cannot be unnamed and its name cannot be changed. `Frog`s have a member function to spawn a number of tadpoles, all unnamed, stored in a vector. - -While this example is fairly simple conceptually, it provides a number of problems in the context of a Julia-C++ application. Firstly, `Tadpole` depends on `Frog` being defined (as it is the return type of `:evole`) and `Frog` depends on `Tadpole` being defined. While this is perfectly valid in a static context, if we were to add usertypes one-by-one through `jluna`, this may cause problems. - -Secondly, translating the exact functionality to Julia isn't trivial. Julia doesn't have internal structs (a struct definition inside another struct is evaluated in the same namespace as the outer struct), and julia structtypes do not have member functions in the traditional, C++ sense. While a field of a Julia structtype can be a function, it does not have implicit access to the other fields of that type. - -Given these limitations, we decide to translate `Frog` and `Tadpole` "ourself", that is without going through the usertype interface. This section will guide users potentially looking at much more complex examples through this process. - -#### Defining the Julia-Side Types - -Independent of `jluna` or C++, we create two structs, also name `Frog` and `Tadpole` that attempt to translate the functionality as closely as possible. Whether this is the right approach is immaterial to this exercise. - -Tadpole has a setter and getter for its property `_name`. Because this is the only field, we can declared Tadpole `mutable`: - -```Julia -mutable struct Tadpole - _name::String -end -``` - -Next we add the "member function" `evolve`. A good paradigm to use for situations like this is the following: - -```Julia -# C++: -auto instance = InstanceType(); -instance.member_function(arguments); - -# Julia: -instance = InstanceType() -InstanceType.member_function(instance, arguments) -``` - -By making the `this` available through an argument, the member function has access to all the same things the C++ member function would have. Applying this design pattern to `Tadpole::evolve`: - -```Julia -function evolve(instance::Tadpole) - return Frog(instance._name) -end - -mutable struct Tadpole - _name::String -end -``` - -Because we want `evolve` to only be available through `Tadpole`, we can make it a member and add it to the constructor: - -```julia -mutable struct Tadpole - _name::String - evolve::Function - - Tadpole() = new( - "", - (instance::Tadpole) -> return Frog(instance._name) - ) -end -``` - -Where we used the `->` syntax to make `evolve` and anonymous function that binds to `Tadpole.evolve`. - -Turning our attention to `Frog`, now, we note that `Frog` only has a single field `_name` that only provides a getter, no setter. This behavior can be best emulated in julia using an immutable struct: +--- -```julia -struct Frog - _name::String -end -``` +## Performance -We add the constructor and `spawn` function: +This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library though is a much fairer comparison and in that aspect it can do quite well, though it is important to be aware of how to avoid overhead and when it is worth to stay with the C-library. -```julia -struct Frog - _name::String - spawn::Function - - Frog(name::String) = new( - name, - (number::Int64) -> return [Tadpole() for _ in 1:number] - ) -end +### Cheat Sheet + +Here, we provide a grading of most of `jluna`s features in terms of runtime performance. `A+` means there is literally 0-overhead, `F` means "do not use this in performance critical code": + +```cpp +// function or feature // grade + +// ### executing julia code +jluna::call A+ +jluna::safe_call A +Proxy::call A+ +Proxy::safe_call A +Proxy::operator() A +jluna::eval A- +jluna::safe_eval A- +State::eval B +State::safe_eval B +Module::eval B +Module::safe_eval B +State::eval_file C+ +State::safe_eval_file C +GeneratorExpression F + +// ### accessing julia-side value +Array::operator[](size_t) A+ +Proxy::operator Any*() A+ +Type::operator jl_datatype_t*() A+ +State::safe_return A +Proxy::operator[] A- +State::new_named_* A +jl_get_function A+ +jl_find_function F + +// ### box / unbox +primitives (int, float, etc.) A+ +const char* A+ +Proxy A +Array A +std::vector A +std::string A +Pair B +Tuple B +Set B- +map / unordered map C +lambdas F // but calling is A +Usertype ?* + +* Usertype boxing performance entirely dependend on user-supplied getter/setter + +// ### other +State::initialize F +GCSentinel A+ +State::set_gc_enabled A+ +State::collect_garbage A+ +jluna::Type (all) A +jluna::register_function F // but calling is A- +Usertype::add_field D +Usertype::enable F +Usertype::implement F ``` -Where we used a generator expression to create a vector of length `number`, that we fill with freshly constructed tadpoles. - -#### Defining Box/Unbox - -Now that we have our Julia-side Tadpole and Frog, we need to connect them to C++ in some way. As stated before, the only thing that needs to be implemented for any C++ object to be transferable to julia is `box` and `unbox`. +### Tips -All box/unbox calls have to adhere to the following signatures: - -```cpp -template -Any* box(T); +The following suggestions are good to keep in mind when writing performance-critical code: -template -T box(Any*); -``` +### Avoid String Parsing -Experienced C++ users may notice that the latter would be ambigous if we were to define it for more than one `T`, because in C++, a functions signature can only be differentiated using the arguments, not the return type. To resolve this, we use [SFINAE](https://en.cppreference.com/w/cpp/language/sfinae) and [concepts](https://en.cppreference.com/w/cpp/language/constraints). Filling it out for `Frog`: +It is often easy to view `jluna` like we would the REPL, controlling Julia through commands supplied as strings. While this is of course possible, it is rarely the fastest way to trigger action Julia side. Notably, when calling Julia functions, often the following pattern achieves much better performance: ```cpp -// using SFINAE: -template, Bool> = true> -Any* box(T); +// 1) as fast as possible +static jl_function_t* call_function = jl_find_function("Main.MyModule", "call_function"); +jluna::call(call_function); -template, Bool> = true> -T unbox(Any*); - -// using Concepts: -template T> -Any* box(T); +// 2) slightly slower +auto call_function = Main["MyModule"]["call_function"]; +call_function(); -template T> -T unbox(Any*); +// 3) slowest +jluna::eval("Main.MyModule.call_function()"); ``` -Where the latter is obviously much nicer syntax and thus preferred. - -TODO - - - - - - - - - ---- - -## Performance - -This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library though is a much fairer comparison and in that aspect it can do quite well, though it is important to be aware of how to avoid overhead and when it is worth to stay with the C-library. - -The following suggestions are good to keep in mind when writing performance-critical code: +Where, `static` is used, such that the `call_function` pointer is only assigned exactly once. Because we control the functions purely through C in `1)`, the julia parser does not come into play at all, avoiding unnecessary overhead incurred by it. ### Stay Julia-Side @@ -2246,31 +2151,6 @@ All performance critical code should be inside a julia file. All C++ should do, By far the easiest way to completely tank a programs' performance is to unnecessarily box/unbox values constantly. If our program requires operation `A` , `B`, `C` julia-side and `X`, `Y`, `Z` C++-side, it's very important to execute everything in a way that minimizes the number of box/unbox calls. So: `ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls. -Of course, we can't completely avoid it, either. To illustrate which box/unbox calls should be used as little as possible and which are perfectly fine, we will grade them in terms of how well they perform (where `A+` is "0 overhead" and `F` is "do not use this in performance critical code"): - -``` -// type T of box/unbox // grade - -int, size_t, float, etc A+ -const char* A+ -Proxy A -vector A -std::string A -Pair B -Set B- -Tuple C -(unordered) map D -lambdas F - -Usertype ? -``` - -Vectors and primitives can be directly swapped between language, which explains their excellent grade, while more complex objects need to be serialized or wrapped before being transferable. - -To make lambdas callable through julia, they have to through `jlunas` C-adapter library to then be wrapper Julia-side, which is too inefficient to be usable in performance critical code. However this only applies to boxing/unboxing the lambdas, usually we only need to box a lambda once, however after that, **calling already boxed lambdas is decently fast**. - -The performance of boxing/unboxing through the `Usertype` interface wholly depends on how optimal the user-defined getter/setters are implemented. Because of this, it is impossible to assign a grade. - ### Minimize Proxy Construction The main overhead incurred by `jluna` is that of safety. Anytime a proxy is constructed, its value and name have to be added to an internal state that protects them from the garbage collector. This takes some amount of time; internally, the value is wrapped and a reference to it and its name is stored in a dictionary. Neither of these is that expensive but when a function uses hundreds of proxies over its runtime, this can add up quickly. Consider the following: diff --git a/include/state.hpp b/include/state.hpp index 6670bea..6281e1e 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -57,7 +57,6 @@ namespace jluna::State /// @returns proxy to result, if any Proxy safe_eval_file(const std::string& path) noexcept; - /// @brief access a value, equivalent to unbox(jl_eval_string("return " + name)) /// @tparam T: type to be unboxed to /// @param full name of the value, e.g. Main.variable._field[0] diff --git a/include/usertype.hpp b/include/usertype.hpp index 5dde4c9..bbd27de 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -90,4 +90,59 @@ namespace jluna }; } -#include ".src/usertype.inl" \ No newline at end of file +#include ".src/usertype.inl" + +function set_xor(set::Set{T}) where T + + res = undef + front::Bool = true + for e in set + if front + res = e + front = false + else + res = Base.xor(res, e) + end + end + return res +end + +function solve(set_in::Set, k::Integer) ::Set{Set} + + pool = Vector{Set{Set}}() + push!(pool, Set()) + + # seed the pool with all sets of size 1 + for n in set_in + push!(pool[1], Set([n])) + end + + # grow sets to generate all possible sets of size k-1 + index = 2 + while index <= k-1 + + push!(pool, Set{Set}()) + for set in pool[index-1] + for n in set_in + push!(pool[index], union(set, Set([n]))) + end + end + index += 1 + end + + # reject some k-1 because: + # For a set s = {s1, s2, ..., s3} such that xor(s) != 0, m = xor(s): xor(union(s, m)) = 0 + out = Set{Set}() + for set in pool[length(pool)] + + xor_result = set_xor(set) + if !(xor_result in set) && (xor_result in set_in) + push!(out, union(set, Set([xor_result]))) + end + end + + return out +end + +# usage: +solve(Set([x for x in 1:256]), 5) \ No newline at end of file From 87bfbe48fc6a6e389e9fa710923c7a2db3cf3a1a Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 5 Mar 2022 20:40:40 +0100 Subject: [PATCH 47/56] second draft of docs & manual done --- .src/usertype.inl | 6 +++ .test/main.cpp | 111 ++++++++++++++++++++-------------------- docs/manual.md | 107 ++++++++++++++++++++------------------ include/gc_sentinel.hpp | 19 +++---- include/jluna.jl | 1 - include/usertype.hpp | 75 +++++---------------------- 6 files changed, 142 insertions(+), 177 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 4d7c049..365350b 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -80,6 +80,12 @@ namespace jluna jl_gc_unpause; } + template + bool Usertype::is_implemented() + { + return _implemented; + } + template Any* Usertype::box(T& in) { diff --git a/.test/main.cpp b/.test/main.cpp index fdd8922..f603235 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -79,59 +79,10 @@ using namespace jluna; int main() { State::initialize(); - /* - Usertype::enable("RGBA"); - Usertype::add_property( - "_red", - [](RGBA& in) -> float {return in._red;}, - [](RGBA& out, float in) -> void {out._red = in;} - ); - Usertype::add_property( - "_green", - [](RGBA& in) -> float {return in._green;}, - [](RGBA& out, float in) -> void {out._green = in;} - ); - Usertype::add_property( - "_blue", - [](RGBA& in) -> float {return in._blue;}, - [](RGBA& out, float in) -> void {out._blue = in;} - ); - Usertype::add_property( - "_alpha", - [](RGBA& in) -> float {return in._alpha;}, - [](RGBA& out, float in) -> void {out._alpha = in;} - ); - - Usertype::add_property( - "_value", - [](RGBA& in) -> float { - float max = 0; - for (float v : {in._red, in._green, in._blue}) - max = std::max(v, max); - return max; - } - ); - - Usertype::implement(); - */ - - auto instance = RGBA(); -instance._red = 1; -instance._blue = 1; - -State::new_named_undef("julia_side_instance") = box(instance); -jluna::safe_eval(R"( - println(julia_side_instance) - julia_side_instance._blue = 0.5; -)"); - -auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instance")); -std::cout << cpp_side_instance._blue << std::endl; - - return 0; - */ Test::initialize(); + + /* Test::test("catch c exception", [](){ Test::assert_that_throws([](){ @@ -1203,6 +1154,7 @@ std::cout << cpp_side_instance._blue << std::endl; i += 1; } }); + */ struct NonJuliaType { @@ -1211,15 +1163,64 @@ std::cout << cpp_side_instance._blue << std::endl; Test::test("Usertype: throw on disable", [](){ - Test::assert_that_throws([](){ - volatile box(NonJuliaType()); + Test::assert_that_throws>([](){ + volatile auto* res = box(NonJuliaType()); }); - Test::assert_that_throws([](){ - volatile unbox(jl_nothing); + Test::assert_that_throws>([](){ + volatile NonJuliaType t = unbox(jl_nothing); }); }); + Test::test("Usertype: enable", [](){ + + Usertype::enable("NonJuliaType"); + Test::assert_that(Usertype::is_enabled()); + }); + + Test::test("Usertype: add property", [](){ + + Usertype::add_property>( + "_field", + [](NonJuliaType& in) -> std::vector { + return in._field; + } + ); + + Usertype::add_property>( + "_field", + [](NonJuliaType& in) -> std::vector { + return in._field; + }, + [](NonJuliaType& out, std::vector in) -> void{ + out._field = in; + } + ); + }); + + Test::test("Usertype: implement", [](){ + Usertype::implement(); + Usertype::implement(); + + Test::assert_that(Usertype::is_implemented()); + }); + + Test::test("Usertype: box/unbox", [](){ + auto instance = NonJuliaType{{123, 34556, 12321}}; + auto sentinel = GCSentinel(); + + auto* res01 = Usertype::box(instance); + auto* res02 = box(instance); + + Test::assert_that(jl_is_equal(res01, res02)); + + auto backres01 = Usertype::unbox(res01); + auto backres02 = unbox(res02); + + Test::assert_that(backres01._field.size() == backres02._field.size()); + }); + + Test::conclude(); } diff --git a/docs/manual.md b/docs/manual.md index 651b560..d431b71 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -56,14 +56,14 @@ Please navigate to the appropriate section by clicking the links below: 10.6 [~~Type Info: Methods~~](#methods)
10.7 [~~Type Info: Properties~~](#properties)
10.8 [Type Classification](#type-classification)
-11. [~~Expressions~~](#expressions)
-12. [Usertypes](#usertypes)
+11. [~~**Expressions**~~](#expressions)
+12. [**Usertypes**](#usertypes)
12.1 [Usertype Interface](#usertype-interface)
12.2 [Making the Type Compliant](#step-1-making-the-type-compliant)
12.3 [Enabling the Interface](#step-2-enabling-the-interface)
12.4 [Adding Property Routines](#step-3-adding-property-routines)
12.5 [Implementing the Type](#step-4-implementing-the-type)
-13. [~~**Paralell Execution~~](#performance)
+13. [~~**Paralell Execution**~~](#performance)
14. [**Performance**](#performance)
14.1 [Cheat Sheet](#cheat-sheet)
14.2 [Avoiding String Parsing](#avoid-string-parsing)
@@ -1847,7 +1847,7 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes -So far, we were only able to box and unbox types that were already supported by `jluna`. While this list of types is broad, in more specialized applications it is sometimes necessary to define our own boxing/unboxing routines, such that we can (un)box arbitrary types. This section will give guidance on how to achieve this. +So far, we were only able to box and unbox types that were already supported by `jluna`. While this list of types is broad, in more specialized applications, it is sometimes necessary to define our own boxing/unboxing routines. Luckily, `jluna` offers an easy-to-use and safe interface for transferring arbitrary C++ types between states. This section will give guidance on its usage. ### Usertype Interface @@ -1863,11 +1863,11 @@ struct RGBA }; ``` -To make classes like this unboxable, we can use the `jluna::Usertype`, a usertype interface that provides a safe, easy way to transfer classes like this between C++ and Julia. +While it may be possible to translate this class into a Julia-side `NamedTuple`, this is rarely the best option. For more complex classes, this is often not possible at all. To make classes like this (un)boxable, we use `jluna::Usertype`. #### Step 1: Making the Type compliant -For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no default constructor, so we need to add it: +For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no explicit default constructor, so we need to add it: ```cpp struct RGBA @@ -1886,11 +1886,11 @@ struct RGBA }; ``` -Furthermore, `T` cannot be (un)boxable via other functions in `jluna`. This means `T` can't be any type in [this list of (un)boxables](#list-of-unboxables). +Furthermore, `T` cannot be already (un)boxable via other `jluna` functions except for other `Usertype`-managed classes. This means `T` can't be any type in [this list of (un)boxables](#list-of-unboxables). #### Step 2: Enabling the Interface -By default, the statement `box(/*...*/)` and `unbox(/*...*/)` compiles for any `T`. However if we try to do this with our `RGBA` class: +By default, the statement `box(/*...*/)` and `unbox(/*...*/)` compiles for any `T`. However, if we try this with our `RGBA` class: ```cpp auto instance = RGBA(); @@ -1902,18 +1902,19 @@ terminate called after throwing an instance of 'jluna::UsertypeNotEnabledExcepti signal (6): Aborted ``` -An exception is thrown at runtime. To avoid unintended behavior, `jluna` requires users to manually enable the usertype interface for any specific type. We can do so using: +An exception is thrown. To avoid unintended behavior, `jluna` requires users to manually enable the usertype interface for any specific type. We do so using: ```cpp Usertype::enable("RGBA"); ``` -This functions takes the julia-side name as an argument. After unboxing, this will be the name of the type, `RGBA` will be converted to. + +Where the argument is the corresponding Julia-side types name, after unboxing. #### Step 3: Adding Property Routines -A *property* of a struct type would be called a "field" in Julia and a "member" in C++. We will use all 3 terms interchangeably here. +A *property* of a struct type would be called a "field" in Julia and a "member" in C++, it any field of any type that is namespaced within a structtype. We'll use all 3 terms, "field", "member", and "property" interchangeably here. -Explaining how to add properties is best done via an example. Thus, we add a property for `_red`: +To add a property for `RGBA`s `_red`, we use: ```cpp Usertype::add_property( @@ -1923,11 +1924,11 @@ Usertype::add_property( ); ``` -Lets talk through this call one-by-one. +Let's talk through this call one-by-one. -First, we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however after `RGBA` is moved to Julia, C++s `float` becomes Julias `Float32`. We can check which C++ types gets converted to which Julia type using `to_julia_type::type_name`. +First, we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however. after `RGBA` is boxed to Julia, C++s `float` becomes Julias `Float32`. We can check which C++ types gets converted to which Julia type using `to_julia_type::type_name`. -This first argument is the **name of the field**. It is best practice to have this be the same name as in C++, `_red` in this case, however there is no mechanism to enforced this. +This first argument is the **name of the field**. It's best practice, to have this be the same name as in C++, `_red` in this case, however there is no mechanism to enforce this. The second argument is the **boxing routine**. This function is called during `box(instance)`. It has the signature `(T&) -> Value_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be boxed: @@ -1935,9 +1936,8 @@ The second argument is the **boxing routine**. This function is called during `b auto instance = RGBA(); Any* boxed = box(instance); -// calls [](RGBA& in) -> float {return in._red;} -// with in = instance -// assign boxed._red with result of boxing routine +// calls [](RGBA& in) -> float {return in._red;} with in = instance +// then assigns boxed._red with result ``` The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type. @@ -1949,14 +1949,12 @@ auto instance = RGBA(); Any* boxed = box(instance); RGBA unboxed = unbox(boxed); -// calls [](RGBA& out, float in) -// where out = unboxed -// in = boxed._red +// calls [](RGBA& out, float in) with out = unboxed, in = unboxed._red ``` -The third argument is optional, if no unboxing routine is unspecified, `unboxed` is not modified. +Specifying the unboxing routine is optional. If left unspecified, no operation is performed during unboxing. -Now that we know how to add fields, let's fully implement the usertype interface for `RGBA`. For completions sake, all previous code is reprinted here: +Now that we know how to add fields, let's fully implement the usertype interface for `RGBA` (where previous code is reprinted here for clarity): ```cpp struct RGBA @@ -1965,7 +1963,7 @@ struct RGBA float _green = 0; float _blue = 0; float _alpha = 1; - // is default constructable + // is default constructable because all members are }; Usertype::enable("RGBA"); @@ -1990,7 +1988,7 @@ Usertype::add_property( [](RGBA& out, float in) -> void {out._alpha;} ); ``` -To illustrate that the properties do not have to directly correspond with the C++ class, let's add another paremeter that represents the `value` component of the HSV color system, sometimes also called "lightness", is defined as the maximum of red, green and blue: +To illustrate that properties do not have to directly correspond with members of the C++ class, we'll add another julia-side field that represents the `value` component from the HSV color system (sometimes also called "lightness"). It is defined as the maximum of red, green and blue, given a color in RGBA: ```cpp Usertype::add_property( @@ -2004,17 +2002,17 @@ Usertype::add_property( ); ``` -We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to. C++-Side. +We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to C++-side. #### Step 4: Implementing the Type -Having added all properties to the usertype interface, the only thing left to do is call: +Having added all properties to the usertype interface, we make Julia aware of the interface by calling: ```cpp Usertype::implement() ``` -This creates a new type julia-side that has the same architecture we gave it. For end-users this happens behind the scene, but for illustrations sake, this expression is assembled an evaluated during `implement`: +This creates a new type julia-side that has the architecture we just gave it. For end-users, this happens behind the scene, however internally, the following expression is assembled and evaluated: ```julia mutable struct RGBA @@ -2027,9 +2025,9 @@ mutable struct RGBA end ``` -We see that `jluna` assembled a struct type whos field names and types are as we have specified. The type is also `mutable` and it has a default construct (a constructor that takes no arguments). The default values here are taken from an unmodified instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. +We see that `jluna` assembled a struct type, whose field names and types are as we have specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for all the field are taken from an unmodified, default initialized instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. We furthermore note that the type is `mutable`. -After having now evaluated this expression and defining the type, the following works: +Having evaluated the above expression and thus defining the type, from this point onwards, the following works: ```cpp auto instance = RGBA(); @@ -2050,22 +2048,22 @@ RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) 0.5 ``` -Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`. This includes assigning proxies, calling julia-side functions with C++-side arguments and even using `RGBA` as value types for a `jluna` array. +Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`. This includes assigning proxies, calling julia-side functions with C++-side arguments, and even using `RGBA` as value types for a `jluna` array. --- ## Performance -This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library though is a much fairer comparison and in that aspect it can do quite well, though it is important to be aware of how to avoid overhead and when it is worth to stay with the C-library. +This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library, though, is a much fairer comparison and in that aspect it can do quite well. It is still important to be aware of how to avoid overhead, and when it may be worth it to stay with the C-library. ### Cheat Sheet -Here, we provide a grading of most of `jluna`s features in terms of runtime performance. `A+` means there is literally 0-overhead, `F` means "do not use this in performance critical code": +Here, we provide a grading of most of `jluna`s features in terms of runtime performance, where `A+` means there is literally 0-overhead, `F` means "do not use this in performance critical code": ```cpp // function or feature // grade -// ### executing julia code +// ### executing julia code ### jluna::call A+ jluna::safe_call A Proxy::call A+ @@ -2081,17 +2079,17 @@ State::eval_file C+ State::safe_eval_file C GeneratorExpression F -// ### accessing julia-side value +// ### accessing julia-side values ### Array::operator[](size_t) A+ Proxy::operator Any*() A+ -Type::operator jl_datatype_t*() A+ State::safe_return A -Proxy::operator[] A- +Proxy::operator[](size_t) A +Proxy::operator[](std::string) A- State::new_named_* A jl_get_function A+ jl_find_function F -// ### box / unbox +// ### box / unbox for T = ### primitives (int, float, etc.) A+ const char* A+ Proxy A @@ -2105,19 +2103,20 @@ map / unordered map C lambdas F // but calling is A Usertype ?* -* Usertype boxing performance entirely dependend on user-supplied getter/setter +* Usertype boxing performance is entirely dependend on user-supplied getter/setter -// ### other +// ### other ### State::initialize F GCSentinel A+ State::set_gc_enabled A+ State::collect_garbage A+ -jluna::Type (all) A +jluna::Type Introspection A jluna::register_function F // but calling is A- Usertype::add_field D Usertype::enable F Usertype::implement F ``` +--- ### Tips @@ -2125,10 +2124,10 @@ The following suggestions are good to keep in mind when writing performance-crit ### Avoid String Parsing -It is often easy to view `jluna` like we would the REPL, controlling Julia through commands supplied as strings. While this is of course possible, it is rarely the fastest way to trigger action Julia side. Notably, when calling Julia functions, often the following pattern achieves much better performance: +It's easy to fall into treating `jluna` like the REPL: a tool to control Julia through the use of commands supplied as strings. This is rarely the most optimal way to trigger Julia-side actions, however. Notably, when calling Julia functions, often the following pattern, of calling functions directly through C, achieves much better performance: ```cpp -// 1) as fast as possible +// 1) exactly as fast as pure julia static jl_function_t* call_function = jl_find_function("Main.MyModule", "call_function"); jluna::call(call_function); @@ -2140,16 +2139,16 @@ call_function(); jluna::eval("Main.MyModule.call_function()"); ``` -Where, `static` is used, such that the `call_function` pointer is only assigned exactly once. Because we control the functions purely through C in `1)`, the julia parser does not come into play at all, avoiding unnecessary overhead incurred by it. +Where `static` was used, so the `call_function` pointer is only assigned exactly once over the course of runtime. ### Stay Julia-Side -All performance critical code should be inside a julia file. All C++ should do, is to manage data and dispatch the julia-side function. For example, let's say we are working on very big matrices. Any matrix operation should happen inside a julia function, and the actual data of the matrix should stay julia-side as much as possible. We can still control julia from C++, `State::eval` has almost no overhead. Similarly, `jluna::Proxy` is just a stand-in for julia-side data. Thus, using julia functions of `jluna::Proxy`s incurs very little overhead compared to doing both only in julia. +All performance critical code should be inside a Julia file. All C++ should do, is to manage data and dispatch the Julia-side functions. For example, let's say we are working on very big matrices. Any matrix operation should happen inside a julia function, the actual data of the matrix should stay julia-side as much as possible. We can still control julia from C++, `State::eval` has almost no overhead. Similarly, `jluna::Proxy` is just a stand-in for julia-side data. Thus, using julia functions of `jluna::Proxy`s incurs very little overhead compared to doing both only in julia. ### Minimize (Un)Boxing -By far the easiest way to completely tank a programs' performance is to unnecessarily box/unbox values constantly. If our program requires operation `A` , `B`, `C` julia-side and `X`, `Y`, `Z` C++-side, it's very important to execute everything in a way that minimizes the number of box/unbox calls. So: -`ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls. +By far the easiest way to completely tank a programs' performance is to unnecessarily box/unbox values constantly. If our program requires operation `A`, `B`, `C` julia-side and `X`, `Y`, `Z` C++-side, it's very important to execute everything in a way that minimizes the number of box/unbox calls. So: +`ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls, because of this it sometimes encouraged to not shy away from handling pure `Any*`, just make sure to also manually control the garbage collector. ### Minimize Proxy Construction @@ -2173,7 +2172,7 @@ Proxy a; } ``` -Where each declaration triggers a proxy to be created. At the end of the block, all no proxies are deallocated because the value of a depends on it's host, `vector_var`. +Where each declaration triggers a proxy to be created. At the end of the block, no proxies are deallocated, because the value of `a` depends on its host, `vector_var`, which needs to stay in scope for `a` to be able to be dereferenced. If we instead access the value like so: @@ -2181,15 +2180,21 @@ If we instead access the value like so: size_t a = State::safe_return("Main.Module1.Module2.vector_var[1].field") ``` -We do not create any proxy at all, increasing performance by up to 6 times. Of course, doing this, we loose the convenience of being assignable, castable, and all other functionalities a named `jluna::Proxy` offers. Still, in performance-critical code, unnamed proxies are almost always faster than named proxies and should be preferred. +We do not create any proxy at all, increasing performance by up to 6 times. Of course, doing this, we loose the convenience of being assignable, castable, and all other functionalities a named `jluna::Proxy` offers. Still, in performance-critical code, unnamed proxies are almost always faster than named proxies and should be preferred. A good middle-ground is the following style: + +```cpp +auto a_proxy = jluna::Proxy(jluna::safe_eval("Main.Module1.Module2.vector_var[1].field")); +size_t a_value = a_proxy; +``` +This calls the proxy constructor with a pure Any* returned through `safe_eval`, reducing the number of proxies from 6 to 1. ### Use the C-Library -When performance needs to be optimal, not "good" or "excellent, but mathematically optimal, it is sometimes necessary to resort to the C-library. Values are hard to manage, the syntax is very clunky and the garbage collector will probably steal many of our values from under our nose and segfault the program, but that is the trade-off of performance vs convenience.
+When performance needs to be optimal, not "good" or "excellent, but mathematically optimal, it is sometimes necessary to resort to the C-library. Values are hard to manage, the syntax is very clunky and the garbage collector will probably steal many of our values from under our nose and segfault the program, but, that is the trade-off of performance vs. convenience.
Luckily the C-library and `jluna` are freely mixable, though we may need to `.update` any proxies whos julia-side value was modified outside `jluna`. ### In Summary -`jluna`s safety features incur an unavoidable overhead. Great care has been taken to minimize this overhead as much as possible, but it is still there. Knowing this, `jluna` is still perfectly fine for most applications. If a part of a library truly is performance critical, however, it may be necessary to avoid using `jluna` as much as possible in order for julia to do, what it's best at: being very fast. When mixing C++ and Julia, though, `jluna` does equally well at bridging that gap, and in many ways it does so better than the C-API. +`jluna`s safety features incur an unavoidable overhead. Great care has been taken to minimize this overhead as much as possible, but it is still non-zero in many cases. Knowing this, `jluna` is still perfectly fine for most applications. If a part of a library is truly performance critical, however, it may be necessary to avoid using `jluna` as much as possible in order for julia to do, what it's best at: being very fast. When mixing C++ and Julia, however, `jluna` does equally well at bridging that gap, and in many ways it does so better than the C-API. --- \ No newline at end of file diff --git a/include/gc_sentinel.hpp b/include/gc_sentinel.hpp index 7c6b7e6..e67ce1f 100644 --- a/include/gc_sentinel.hpp +++ b/include/gc_sentinel.hpp @@ -15,15 +15,16 @@ namespace jluna private: bool _before; - GCSentinel() - { - _before = jl_gc_is_enabled(); - jl_gc_enable(false); - } + public: + GCSentinel() + { + _before = jl_gc_is_enabled(); + jl_gc_enable(false); + } - ~GCSentinel() - { - jl_gc_enable(_before); - } + ~GCSentinel() + { + jl_gc_enable(_before); + } }; } \ No newline at end of file diff --git a/include/jluna.jl b/include/jluna.jl index db756f5..0410899 100644 --- a/include/jluna.jl +++ b/include/jluna.jl @@ -993,7 +993,6 @@ module jluna end push!(out.args[3].args, new_call) - println(out) Base.eval(m, out) return m.eval(template._typename) end diff --git a/include/usertype.hpp b/include/usertype.hpp index bbd27de..2cc7019 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -24,6 +24,7 @@ namespace jluna /// @brief original type using original_type = T; + /// @brief ctor delete, static-only interface Usertype() = delete; /// @brief enable interface @@ -36,10 +37,8 @@ namespace jluna /// @brief add field /// @param name: julia-side name of field - /// @param type: type of symbol. User the other overload if the type is a typevar, such as "P" (where P is a parameter) /// @param box_get: lambda with signature (T&) -> Any* - /// @param unbox_set: lambda with signature (T&, Any*) - /// @note this function will throw if called after implement() + /// @param unbox_set: lambda with signature (T&, Any*) -> void template static void add_property( const std::string& name, @@ -47,15 +46,24 @@ namespace jluna std::function unbox_set = noop_set ); - /// + /// @brief create the type, setup through the interface, julia-side + /// @param module: module in which the type is evaluated static void implement(Module module = Main); + /// @brief has implement() been called at least once + /// @returns bool static bool is_implemented(); /// @brief box interface + /// @param T&: instance + /// @returns boxed value + /// @note this function will call implement() if it has not been called before, incurring a tremendous overhead on first execution, once static Any* box(T&); - /// @brief unbox interface + /// @brief box interface + /// @param Any* + /// @returns unboxed value + /// @note this function will call implement() if it has not been called before, incurring a tremendous overhead on first execution, once static T unbox(Any*); private: @@ -90,59 +98,4 @@ namespace jluna }; } -#include ".src/usertype.inl" - -function set_xor(set::Set{T}) where T - - res = undef - front::Bool = true - for e in set - if front - res = e - front = false - else - res = Base.xor(res, e) - end - end - return res -end - -function solve(set_in::Set, k::Integer) ::Set{Set} - - pool = Vector{Set{Set}}() - push!(pool, Set()) - - # seed the pool with all sets of size 1 - for n in set_in - push!(pool[1], Set([n])) - end - - # grow sets to generate all possible sets of size k-1 - index = 2 - while index <= k-1 - - push!(pool, Set{Set}()) - for set in pool[index-1] - for n in set_in - push!(pool[index], union(set, Set([n]))) - end - end - index += 1 - end - - # reject some k-1 because: - # For a set s = {s1, s2, ..., s3} such that xor(s) != 0, m = xor(s): xor(union(s, m)) = 0 - out = Set{Set}() - for set in pool[length(pool)] - - xor_result = set_xor(set) - if !(xor_result in set) && (xor_result in set_in) - push!(out, union(set, Set([xor_result]))) - end - end - - return out -end - -# usage: -solve(Set([x for x in 1:256]), 5) \ No newline at end of file +#include ".src/usertype.inl" \ No newline at end of file From 84f359826a6bd991381988a79e0053979f6bd95f Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 5 Mar 2022 21:04:22 +0100 Subject: [PATCH 48/56] clang12 working --- .test/main.cpp | 2 -- CMakeLists.txt | 6 ++++-- README.md | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index f603235..8a2cc12 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -82,7 +82,6 @@ int main() Test::initialize(); - /* Test::test("catch c exception", [](){ Test::assert_that_throws([](){ @@ -1154,7 +1153,6 @@ int main() i += 1; } }); - */ struct NonJuliaType { diff --git a/CMakeLists.txt b/CMakeLists.txt index edeb673..59ee4a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_BUILD_TYPE Debug) # compiler support if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "12.0.0") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2b -lstdc++") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "10.0.0") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fconcepts") else() @@ -117,7 +117,9 @@ add_library(jluna SHARED include/usertype.hpp .src/usertype.inl - include/gc_sentinel.hpp) + + include/gc_sentinel.hpp +) set_target_properties(jluna PROPERTIES LINKER_LANGUAGE C diff --git a/README.md b/README.md index d9c681f..2e646b3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Julia is a beautiful language, it is well-designed, and well-documented. Julias C-API is also well-designed, less beautiful, and much less... documented. -Heavily inspired in design and syntax by (but in no way affiliated with) the excellent Lua⭤C++ wrapper [**sol2**](https://github.com/ThePhD/sol2), `jluna` aims to fully wrap the official julia C-API, replacing it in usage in C++ projects by making accessing julias unique strengths through C++ safe, hassle-free, and just as beautiful. +Heavily inspired in design and syntax by (but in no way affiliated with) the excellent Lua ⭤ C++ wrapper [**sol2**](https://github.com/ThePhD/sol2), `jluna` aims to fully wrap the official julia C-API, replacing it in usage in projects with C++ as the host language by making accessing julias unique strengths through C++ safe, hassle-free, and just as beautiful. --- @@ -128,9 +128,9 @@ Usertype::add_property( ); Usertype::add_property>( - "_field02", - [](NonJuliaType& in) -> std::vector {return in._field02;}, - [](NonJuliaType& in, std::vector value) {in._field02 = value;} + "_field02", + [](NonJuliaType& in) -> std::vector {return in._field02;}, + [](NonJuliaType& in, std::vector value) {in._field02 = value;} ); // create julia-side equivalent type @@ -156,11 +156,11 @@ Some of the many advantages `jluna` has over the C-API include: + expressive generic syntax + call C++ functions from julia using any julia-type + assigning C++-side proxies also mutates the corresponding variable with the same name julia-side -+ julia-side values, including temporaries, are kept safe from the garbage collector + any C++ type can be moved between Julia and C++ + multi-dimensional, iterable array interface with julia-style indexing + C++-side introspection that is deeper than what is possible through only Julia + fast! All code is considered performance-critical and was optimized for minimal overhead compared to the C-API ++ julia-side values, including temporaries, are kept safe from the garbage collector + verbose exception forwarding, compile-time assertions + inline documentation for IDEs for both C++ and Julia code+ verbose manual, written by a human + freely mix `jluna` and the C-API From 06d6e19f2e2d0c654bac1e3bf22f74285a1f1d9a Mon Sep 17 00:00:00 2001 From: clem Date: Sat, 5 Mar 2022 22:49:34 +0100 Subject: [PATCH 49/56] polish --- .test/main.cpp | 65 -------------------------------------------------- 1 file changed, 65 deletions(-) diff --git a/.test/main.cpp b/.test/main.cpp index 8a2cc12..23e0784 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,75 +11,10 @@ using namespace jluna; using namespace jluna::detail; -struct RGBA -{ - RGBA() - : _red(0), - _green(0), - _blue(0), - _alpha(1) - {} - - float _red; - float _green; - float _blue; - float _alpha; -}; - -class Frog -{ - private: - std::string _name; - - public: - struct Tadpole - { - Tadpole() = default; - - void set_name(const std::string& name) - { - _name = name; - } - - const std::string& get_name() const - { - return _name; - } - - Frog evolve() - { - assert(_name != ""); - return Frog(_name); - } - - private: - std::string _name; - }; - - Frog(const std::string& name) - : _name(name) - {} - - std::vector spawn(size_t number) - { - std::vector out; - for (size_t i = 0; i < number; ++i) - out.push_back(Tadpole()); - - return out; - } - - std::string get_name() const - { - return _name; - } -}; - using namespace jluna; int main() { State::initialize(); - Test::initialize(); Test::test("catch c exception", [](){ From cb0712ca4cfb7d0235d25b018707bb24e8aa6eff Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 01:42:11 +0100 Subject: [PATCH 50/56] stashing --- .src/usertype.inl | 2 +- .test/main.cpp | 59 ++++++- docs/manual.md | 296 +++++++++++++++++++++++++++++++++--- include/gc_sentinel.hpp | 2 + include/julia_extension.hpp | 3 +- 5 files changed, 338 insertions(+), 24 deletions(-) diff --git a/.src/usertype.inl b/.src/usertype.inl index 365350b..d3363a7 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -15,7 +15,7 @@ namespace jluna template UsertypeNotEnabledException::UsertypeNotEnabledException() - : _msg("[C++][Exception] usertype interface for this type was not yet enabled. C Usertype::enable(const std::string&) to instance the interface") + : _msg("[C++][Exception] usertype interface for this type was not yet enabled. Call Usertype::enable(const std::string&) to instance the interface") {} template diff --git a/.test/main.cpp b/.test/main.cpp index 23e0784..80ad14e 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,10 +11,67 @@ using namespace jluna; using namespace jluna::detail; -using namespace jluna; +class Frog +{ + public: + // tadpole, a baby frog + struct Tadpole + { + // name, can be changed + std::string _name; + + // ctor, tadpoles are born without a name + Tadpole() + : _name("") + {} + + // if a tadpole has a name, it can evolve into a frog + Frog evolve() const + { + if (_name == "") + throw std::invalid_argument("tadpole needs to be named before evolving"); + + return Frog(_name); + } + }; + + public: + // a frog can spawn a number of tadpoles + std::vector spawn(size_t n) const + { + std::vector out; + for (size_t i = 0; i < n; ++i) + out.push_back(Frog::Tadpole()); + + return out; + } + + // get name + std::string get_name() + { + return _name; + } + + private: + // private ctor, can only be called by Frog::Tadpole::evolve + Frog(std::string name) + : _name(name) + {} + + // name, cannot be changed because there is no setter + std::string _name; +}; + int main() { State::initialize(); + + auto ted = Frog::Tadpole(); + ted._name = "Ted"; + auto frog = ted.evolve(); + std::cout << frog.get_name() << std::endl; + + return 0; Test::initialize(); Test::test("catch c exception", [](){ diff --git a/docs/manual.md b/docs/manual.md index d431b71..4de6048 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -279,9 +279,9 @@ std::function => function (::Any, ::Any, ::Any) -> std::function => function (::Any, ::Any, ::Any, ::Any) -> Any std::function)> => function (::Vector{Any}) ::Any -Usertype => T * +Usertype::original_type => T ° -* where T is an arbitrary C++ type +° where T is an arbitrary C++ type ``` We will learn more on how to box/unbox functions, specifically, in the [section on calling C++ functions from julia](#functions). We can (un)box truly arbitrary C++ types through the [usertype interface](#usertypes), which we will explore later on, aswell. @@ -1847,7 +1847,7 @@ Once fully unrolled, we have access to the properties necessary for introspect. ## Usertypes -So far, we were only able to box and unbox types that were already supported by `jluna`. While this list of types is broad, in more specialized applications, it is sometimes necessary to define our own boxing/unboxing routines. Luckily, `jluna` offers an easy-to-use and safe interface for transferring arbitrary C++ types between states. This section will give guidance on its usage. +So far, we were only able to box and unbox types that were already supported by `jluna`. While this list of types is broad, in more specialized applications it is sometimes necessary to define our own boxing/unboxing routines. Luckily, `jluna` offers an easy-to-use and safe interface for transferring arbitrary C++ types between states. This section will give guidance on how to use it. ### Usertype Interface @@ -1863,11 +1863,11 @@ struct RGBA }; ``` -While it may be possible to translate this class into a Julia-side `NamedTuple`, this is rarely the best option. For more complex classes, this is often not possible at all. To make classes like this (un)boxable, we use `jluna::Usertype`. +While it may be possible to manually translate this class into a Julia-side `NamedTuple`, this is rarely the best option. For more complex classes, this is often not possible at all. To make classes like this (un)boxable, we use `jluna::Usertype`. #### Step 1: Making the Type compliant -For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no explicit default constructor, so we need to add it: +For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no explicit default constructor, so we should add it: ```cpp struct RGBA @@ -1898,7 +1898,7 @@ auto* res = box(instance); ``` ``` terminate called after throwing an instance of 'jluna::UsertypeNotEnabledException' - what(): [C++][Exception] usertype interface for this type was not yet enabled. C Usertype::enable(const std::string&) to instance the interface + what(): [C++][Exception] usertype interface for this type was not yet enabled. Call Usertype::enable(const std::string&) to instance the interface signal (6): Aborted ``` @@ -1908,19 +1908,23 @@ An exception is thrown. To avoid unintended behavior, `jluna` requires users to Usertype::enable("RGBA"); ``` -Where the argument is the corresponding Julia-side types name, after unboxing. +Where the argument is the corresponding Julia-side types name, after unboxing. Note that this type does not have to be defined at this point, we will take care of that in later steps. #### Step 3: Adding Property Routines -A *property* of a struct type would be called a "field" in Julia and a "member" in C++, it any field of any type that is namespaced within a structtype. We'll use all 3 terms, "field", "member", and "property" interchangeably here. +A *property* of a struct type is what would be called a "field" in Julia and a "member" in C++. It is any named variable of any type (including functions) that is namespaced within a struct. We'll use all three terms, "field", "member", and "property" interchangeably here. To add a property for `RGBA`s `_red`, we use: ```cpp Usertype::add_property( "_red", - [](RGBA& in) -> float {return in._red;}, - [](RGBA& out float in) -> void {out._red;} + [](RGBA& in) -> float { + return in._red; + }, + [](RGBA& out float in) -> void { + out._red; + } ); ``` @@ -1928,9 +1932,9 @@ Let's talk through this call one-by-one. First, we have the **template parameter**, `float` in this case. This is the type of the field. We use C++ types here, however. after `RGBA` is boxed to Julia, C++s `float` becomes Julias `Float32`. We can check which C++ types gets converted to which Julia type using `to_julia_type::type_name`. -This first argument is the **name of the field**. It's best practice, to have this be the same name as in C++, `_red` in this case, however there is no mechanism to enforce this. +This first argument is the **name of the field**. It is best practice, to have this be the same name as the field in C++, `_red` in this case, however, there is no mechanism to enforce this. -The second argument is the **boxing routine**. This function is called during `box(instance)`. It has the signature `(T&) -> Value_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be boxed: +The second argument is the **boxing routine**. This function is called during `box(instance)`. It has the signature `(T&) -> Property_t`, which for `RGBA` and this specific field becomes `(RGBA&) -> float`. The argument of the boxing routine is the instance of the type that is about to be boxed: ```cpp auto instance = RGBA(); @@ -1940,9 +1944,9 @@ Any* boxed = box(instance); // then assigns boxed._red with result ``` -The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type. +The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type when `box` is called. -The third argument is the **unboxing routine**. This lambda has the signature `(T&, Value_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the resulting cpp-side instance, the second argument is the value of the field of the corresponding name: +The third argument is the **unboxing routine**. This lambda has the signature `(T&, Property_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the cpp-side instance that is the result of the `unbox` call, the second argument is the value of the field of the corresponding name of the julia-side instance. ```cpp auto instance = RGBA(); @@ -2002,17 +2006,17 @@ Usertype::add_property( ); ``` -We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to C++-side. +We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign for C++-side instaces. #### Step 4: Implementing the Type Having added all properties to the usertype interface, we make Julia aware of the interface by calling: ```cpp -Usertype::implement() +Usertype::implement(); ``` -This creates a new type julia-side that has the architecture we just gave it. For end-users, this happens behind the scene, however internally, the following expression is assembled and evaluated: +This creates a new type julia-side that has the architecture we just gave it. For end-users, this happens behind the scene, however, internally, the following expression is assembled and evaluated: ```julia mutable struct RGBA @@ -2025,21 +2029,23 @@ mutable struct RGBA end ``` -We see that `jluna` assembled a struct type, whose field names and types are as we have specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for all the field are taken from an unmodified, default initialized instance of `T` (`RGBA()`) in our case. This is why the type needs to be default constructible. We furthermore note that the type is `mutable`. +We see that `jluna` assembled a struct type, whose field names and types are as we have specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for all the field are taken from an unmodified, default initialized instance of `T` (`RGBA()` in our case). This is why the type needs to be default constructible. We furthermore note that the julia-side type is `mutable`. -Having evaluated the above expression and thus defining the type, from this point onwards, the following works: +Having evaluated the above expression, we fully implemented the usertype interface for `RGBA`. From this point onwards, the following works: ```cpp auto instance = RGBA(); instance._red = 1; instance._blue = 1; -State::new_named_undef("julia_side_instance") = box(instance); +// now boxable +State::new_named_undef("julia_side_instance") = box(instance); jluna::safe_eval(R"( println(julia_side_instance) julia_side_instance._blue = 0.5; )"); +// and unboxable auto cpp_side_instance = unbox(jluna::safe_eval("return julia_side_instance")); std::cout << cpp_side_instance._blue << std::endl; ``` @@ -2048,7 +2054,255 @@ RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) 0.5 ``` -Because `box` and `unbox` are now defined, all of `jluna`s functionality now also works with `RGBA`. This includes assigning proxies, calling julia-side functions with C++-side arguments, and even using `RGBA` as value types for a `jluna` array. +Because `box` and `unbox` are now defined, all of `jluna`s other functionality now also works with `RGBA`. This includes assigning proxies, casting proxies to `RGBA`, calling julia-side functions with C++-side arguments, even using `RGBA` as value types for a `jluna` array, etc. + +### Manual Implementation + +`jluna::Usertype` works great if we have a C++-side type that we want to move to julia because it creates a completely new type from the C++-type. However, what if we already have an existing julia type and we want to map an arbitrary C++ type to it? We could, of course, box the C++-type to the type `Usertype` created, then convert that type to our julia type. While this is a decent option for most users, it is suboptimal performance wise. In cases like these, we will have to resort to manually implementing box/unbox. This section will work through an example, giving users a template on how to achieve this. + + +#### Example: Tadpoles & Frogs + +Let's first introduce our example: Consider the following classes: + +```cpp +class Frog +{ + public: + // tadpole, a baby frog + struct Tadpole + { + // name, can be changed + std::string _name + + // ctor, tadpoles are born without a name + Tadpole() + : _name("") + {} + + // if a tadpole has a name, it can evolve into a frog + Frog evolve() const + { + assert(_name != ""); + return Frog(_name); + } + }; + + public: + // a frog can spawn a number of tadpoles + std::vector spawn(size_t n) const + { + std::vector out; + for (size_t i = 0; i < n; ++i) + out.push_back(Frog::Tadpole()); + + return out; + } + + // get name + std::string get_name() + { + return _name; + } + + private: + // private ctor, can only be called by Frog::Tadpole::evolve + Frog(std::string name) + : _name(name) + {} + + // name, cannot be changed because there is no setter + std::string _name; +}; +``` + +These classes are deceptively simple, however they provide some unique challenges. We cannot instance a `Frog` (henceforth "frog"), the only way to construct a frog is by first creating a `Frog::Tadpole` (henceforth "tadpole"), naming it, then calling `evolve`. Because tadpole is an internal class of `Frog`, it has access to the private constructor. Furthermore, while we can access a frogs name, the name cannot be changed. The name of a tadpole can be both access and changed. + +Let's say we want to translate this functionality to Julia without using `Usertype`. The julia-versions of the above could be: + +```julia +mutable struct Tadpole # mutable because _name needs to be changable + + # name, can be changed + _name::String + + # "member" function: (::Tadole) -> Frog + evolve::Function + + # ctor without a name + Tadpole() = new( + "", + (this::Tadpole) -> Frog(this._name) + ) +end + +struct Frog # immutable because _name only has getter + + # name, cannot be changed + _name::String + + # member function: (::Integer) -> Vector{Tadpole} + spawn::Function + + # ctor from the tadpole + Frog(as_tadpole::Tadpole) = new( + as_tadpole._name, + (n::Integer) -> [Tadpole() for _ in 1:n] + ) +end +``` + +Where we defined the frogs ctor as only being constructable from a tadpole, which emulates the behavior the classes exhibit C++ side. A bit is lost in translation here as julia usually discourages member functions in the C++ sense, but for the sake of the example we defined them here using the following equivalence: + +```julia +# in cpp: +auto instance = InstanceType(); +instance.member_function(arguments); + +# in julia +instance = InstanceType() +InstanceType.member_function(instance, arguments) +``` + +By making the instance the argument, julia functions inside a struct emulate the C++ behavior of being able to access all members of the struct they are part of. + +### Boxing Definition + +We now want to implement boxing and unboxing for both frogs and tadpoles. When defining these functions for a custom type, they need to adhere to the following signatures exactly: + +```cpp +template T> +Any* box(T); + +template T> +T unbox(Any*); +``` + +Where `Is` is a concept that enforces `T` to be equal to `U`. We need to use concepts and template functions here, so the syntax `box(/*...*/)` and `unbox(/*...*/)` are valid, which is required by most of `jluna`s functions. Given this signature, the `box` definitions for `Frog` and `Frog::Tadpole` look like this: + +```cpp +template T> +Any* box(T in) +{ + +} + +template T> +T unbox(Any* in) +{ + +} +``` + +Because we are handling raw C-pointers and not proxies, we need to manually protect ourself from the garbage collector (GC). This is done best using `jluna::GCSentinel`, which is a class that disables the GC while it is in scope: + +```cpp +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); +} + +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); +} +``` + +To create the julia-side versions of the classes, we need access to their julia-side constructors. We can do so using `jl_find_function`, then using the constructors to create instances: + +```cpp +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + + auto* out = jluna::safe_call(tadpole_ctor); +} + +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* frog_ctor = jl_find_function("main", "Frog"); + + auto* out = jluna::safe_call(frog_tor); // WILL FAIL +} +``` + +But we run into a problem, `Frog` does not have a no-argument CTOR. The only way to construct frog is from a tadpole, however we cannot ask for a tadpole as an argument to the box function because we have to strictly adhere to the `(T) -> Any*` signature. The only way to solve this conundrum is to create a new julia-side function that constructors a frog for us in a round-about way: + +```julia +function generate_frog(name::String) ::Frog + tadpole = Tadpole() + tadpole._name = name + return Frog(tadpole) +end +``` +The example in this section was picked this way, specifically to illustrate how, when the julia-type is pre-defined, we will often do some extra work to make C++ and Julia play nice together. We cannot extend the julia-side `Frog` class, which is why we need to create this wrapper function to construct our frog instead. Accessing and calling the function inside `box>` now looks like this: + +```cpp +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + + auto* out = jluna::safe_call(tadpole_ctor); +} + +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* frog_ctor = jl_find_function("main", "generate_frog"); + + auto* out = jluna::safe_call(frog_tor, box(in._name)); +} +``` + +Where we also had to manually box the string, because we are not dealing with proxies which do this automatically anymore. + +Lastly, we need to update the newly created julia-side instance with the actual values of the C++-side instance, otherwise all `box` would be is a way to construct new frogs and tadpoles. To do this, we use `Base.setfield!`. Note, however, that `Frog` julia-side is immutable. This is why we created the generator function, the only way to make julia-side frogs have the same name as C++-side frogs, is to assign their name on construction. Because of this, the frog instanced in `box` does not need to be modified further: + +```cpp +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + + auto* out = jluna::safe_call(tadpole_ctor); + + static auto* setfield = jl_find_function("Base", "setfield!"); + static auto field_symbol = Symbol("_name"); + jluna::safe_call(setfield, out, (Any*) field_symbol, box(in._name)); + return out; +} + +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* frog_ctor = jl_find_function("main", "generate_frog"); + + auto* out = jluna::safe_call(frog_tor, box(in._name)); + return out; +} +``` + +Where we used `jluna::Symbol` to what needs to be `:_name` julia-side. We specified it as `static` as that creation only needs to happen once over the course of the runtime, saving a little bit of overhead each call. + +#### Implement unbox + +Now that we have box fully written, we can turn our attention to unbox. + + + + + --- diff --git a/include/gc_sentinel.hpp b/include/gc_sentinel.hpp index e67ce1f..a040c18 100644 --- a/include/gc_sentinel.hpp +++ b/include/gc_sentinel.hpp @@ -16,12 +16,14 @@ namespace jluna bool _before; public: + /// @brief ctor, from this point onwards, the GC is disabled GCSentinel() { _before = jl_gc_is_enabled(); jl_gc_enable(false); } + /// @brief dtor, after this is called, the GC state is returned to what it was previously ~GCSentinel() { jl_gc_enable(_before); diff --git a/include/julia_extension.hpp b/include/julia_extension.hpp index 804bb88..d994196 100644 --- a/include/julia_extension.hpp +++ b/include/julia_extension.hpp @@ -105,7 +105,8 @@ extern "C" /// @returns value inline jl_value_t* jl_undef_initializer() { - return jl_eval_string("return undef"); + static jl_function_t* undef_initializer = jl_get_function(jl_base_module, "UndefInitializer"); + return jl_call0(undef_initializer); } /// @brief get nth element of tuple From 8ad2fcce509502c2b3d8785714e3fd12d65fe89e Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 01:59:37 +0100 Subject: [PATCH 51/56] stashing --- docs/manual.md | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/manual.md b/docs/manual.md index 4de6048..85ac6a3 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -2217,7 +2217,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); auto* out = jluna::safe_call(tadpole_ctor); } @@ -2226,7 +2226,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* frog_ctor = jl_find_function("main", "Frog"); + static auto* frog_ctor = jl_find_function("Main", "Frog"); auto* out = jluna::safe_call(frog_tor); // WILL FAIL } @@ -2248,7 +2248,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); auto* out = jluna::safe_call(tadpole_ctor); } @@ -2257,7 +2257,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* frog_ctor = jl_find_function("main", "generate_frog"); + static auto* frog_ctor = jl_find_function("Main", "generate_frog"); auto* out = jluna::safe_call(frog_tor, box(in._name)); } @@ -2272,7 +2272,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* tadpole_ctor = jl_find_function("main", "Tadpole"); + static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); auto* out = jluna::safe_call(tadpole_ctor); @@ -2286,7 +2286,7 @@ template T> Any* box(T in) { auto sentinel = GCSentinel(); - static auto* frog_ctor = jl_find_function("main", "generate_frog"); + static auto* frog_ctor = jl_find_function("Main", "generate_frog"); auto* out = jluna::safe_call(frog_tor, box(in._name)); return out; @@ -2297,7 +2297,40 @@ Where we used `jluna::Symbol` to what needs to be `:_name` julia-side. We specif #### Implement unbox -Now that we have box fully written, we can turn our attention to unbox. +Now that we have box fully written, we can turn our attention to unbox. Unboxing is much simpler, all we need to do is access the property of the julia-side instance pointed to by the argument using `Base.getfield`, then construct the C++-side instance. Because we are back C++-side, we can use `Tadpole::evolve` to create a frog, as intended: + +```cpp +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto out = Tadpole(); + out._name = unbox(julia_side_name); + return out; +} + +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto tadpole = Tadpole(); + tadpole._name = unbox(julia_side_name); + + return tadpole.evolve(); +} +``` + +Where we again allocate the julia-side symbol `:_name` statically to save on allocation per box call. From 69da5c6b8433ce3b23174514dfa88a1b8f9db956 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 16:07:42 +0100 Subject: [PATCH 52/56] stashing before disable rework --- .test/main.cpp | 82 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/.test/main.cpp b/.test/main.cpp index 80ad14e..c32f1e9 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -62,14 +62,94 @@ class Frog std::string _name; }; +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto out = Frog::Tadpole(); + out._name = unbox(julia_side_name); + return out; +} + +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto tadpole = Frog::Tadpole(); + tadpole._name = unbox(julia_side_name); + + return tadpole.evolve(); +} + +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); + + auto* out = jluna::safe_call(tadpole_ctor); + + static auto* setfield = jl_find_function("Base", "setfield!"); + static auto field_symbol = Symbol("_name"); + jluna::safe_call(setfield, out, (Any*) field_symbol, box(in._name)); + return out; +} + +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* frog_ctor = jl_find_function("Main", "generate_frog"); + + auto* out = jluna::safe_call(frog_ctor, box(in._name)); + return out; +} + int main() { State::initialize(); + State::safe_eval(R"( + mutable struct Tadpole + + _name::String + evolve::Function + + Tadpole() = new( + "", + (this::Tadpole) -> Frog(this._name) + ) + end + + struct Frog + + _name::String + spawn::Function + + Frog(as_tadpole::Tadpole) = new( + as_tadpole._name, + (n::Integer) -> [Tadpole() for _ in 1:n] + ) + end + )"); + auto ted = Frog::Tadpole(); + + auto* boxed = box(ted); + Base["println"](boxed); + ted._name = "Ted"; auto frog = ted.evolve(); - std::cout << frog.get_name() << std::endl; return 0; Test::initialize(); From 13598be44bc057e6eebc911a8e182995328df677 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 16:57:34 +0100 Subject: [PATCH 53/56] rework usertype --- .test/main.cpp | 4 ++++ include/concepts.hpp | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.test/main.cpp b/.test/main.cpp index c32f1e9..9aed0cf 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -115,6 +115,9 @@ Any* box(T in) return out; } +disable_usertype(Frog); +disable_usertype(Frog::Tadpole); + int main() { State::initialize(); @@ -152,6 +155,7 @@ int main() auto frog = ted.evolve(); return 0; + Test::initialize(); Test::test("catch c exception", [](){ diff --git a/include/concepts.hpp b/include/concepts.hpp index cbd4bcb..bdf0924 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -131,6 +131,18 @@ namespace jluna template concept IsTuple = detail::is_tuple_aux(std::make_index_sequence::value>()); + /// @concept should be resolved as usertype + template + struct usertype_disabled + { + constexpr static inline bool value = false; + }; + + template + concept HasUsertypeInterfaceDisabled = usertype_disabled::value; + + #define disable_usertype(T) template<> struct jluna::usertype_disabled {constexpr static inline bool value = true;}; + /// @concept is none of the above and default constructible template concept IsUsertype = @@ -148,5 +160,6 @@ namespace jluna and not LambdaType and not LambdaType and not LambdaType - and not LambdaType>; + and not LambdaType> + and not HasUsertypeInterfaceDisabled; } \ No newline at end of file From 9e8b074810c8676debfe5b2f36f610c2e69229c5 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 18:14:08 +0100 Subject: [PATCH 54/56] implemented new usertype deduction --- .src/usertype.inl | 57 +++++------- .test/main.cpp | 168 +--------------------------------- docs/frog_tadpole_example.cpp | 153 +++++++++++++++++++++++++++++++ docs/manual.md | 112 +++++++++++++---------- include/concepts.hpp | 27 +----- include/usertype.hpp | 32 ++----- 6 files changed, 259 insertions(+), 290 deletions(-) create mode 100644 docs/frog_tadpole_example.cpp diff --git a/.src/usertype.inl b/.src/usertype.inl index d3363a7..505b374 100644 --- a/.src/usertype.inl +++ b/.src/usertype.inl @@ -10,32 +10,15 @@ namespace jluna template struct to_julia_type> { - inline static std::string type_name = ""; + static inline const std::string type_name = usertype_enabled::name; }; - template - UsertypeNotEnabledException::UsertypeNotEnabledException() - : _msg("[C++][Exception] usertype interface for this type was not yet enabled. Call Usertype::enable(const std::string&) to instance the interface") - {} template - const char * UsertypeNotEnabledException::what() const noexcept + void Usertype::initialize() { - return _msg.c_str(); - } - - template - void Usertype::enable(const std::string& name) - { - _enabled = true; - _name = std::make_unique(name); - to_julia_type>::type_name = name; - } - - template - bool Usertype::is_enabled() - { - return _enabled; + throw_if_uninitialized(); + _name = std::make_unique(get_name()); } template @@ -45,6 +28,9 @@ namespace jluna std::function box_get, std::function unbox_set) { + if (_name.get() == nullptr) + initialize(); + auto symbol = Symbol(name); if (_mapping.find(name) == _mapping.end()) @@ -61,9 +47,24 @@ namespace jluna }}); } + template + std::string Usertype::get_name() + { + return usertype_enabled::name; + } + + template + bool Usertype::is_enabled() + { + return usertype_enabled::value; + } + template void Usertype::implement(Module module) { + if (_name.get() == nullptr) + initialize(); + jl_gc_pause; static jl_function_t* implement = jl_find_function("jluna", "implement"); static jl_function_t* new_proxy = jl_find_function("jluna", "new_proxy"); @@ -89,9 +90,6 @@ namespace jluna template Any* Usertype::box(T& in) { - if (not _enabled) - throw UsertypeNotEnabledException(); - if (not _implemented) implement(); @@ -110,9 +108,6 @@ namespace jluna template T Usertype::unbox(Any* in) { - if (not _enabled) - throw UsertypeNotEnabledException(); - if (not _implemented) implement(); @@ -131,18 +126,12 @@ namespace jluna template T unbox(Any* in) { - if (not Usertype::is_enabled()) - throw UsertypeNotEnabledException(); - - return Usertype::unbox(in); + return Usertype::unbox(in); } template Any* box(T in) { - if (not Usertype::is_enabled()) - throw UsertypeNotEnabledException(); - return Usertype::box(in); } } \ No newline at end of file diff --git a/.test/main.cpp b/.test/main.cpp index 9aed0cf..63657e1 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,151 +11,9 @@ using namespace jluna; using namespace jluna::detail; -class Frog -{ - public: - // tadpole, a baby frog - struct Tadpole - { - // name, can be changed - std::string _name; - - // ctor, tadpoles are born without a name - Tadpole() - : _name("") - {} - - // if a tadpole has a name, it can evolve into a frog - Frog evolve() const - { - if (_name == "") - throw std::invalid_argument("tadpole needs to be named before evolving"); - - return Frog(_name); - } - }; - - public: - // a frog can spawn a number of tadpoles - std::vector spawn(size_t n) const - { - std::vector out; - for (size_t i = 0; i < n; ++i) - out.push_back(Frog::Tadpole()); - - return out; - } - - // get name - std::string get_name() - { - return _name; - } - - private: - // private ctor, can only be called by Frog::Tadpole::evolve - Frog(std::string name) - : _name(name) - {} - - // name, cannot be changed because there is no setter - std::string _name; -}; - -template T> -T unbox(Any* in) -{ - auto sentinel = GCSentinel(); - static auto* getfield = jl_find_function("Base", "getfield"); - static auto field_symbol = Symbol("_name"); - - Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); - - auto out = Frog::Tadpole(); - out._name = unbox(julia_side_name); - return out; -} - -template T> -T unbox(Any* in) -{ - auto sentinel = GCSentinel(); - static auto* getfield = jl_find_function("Base", "getfield"); - static auto field_symbol = Symbol("_name"); - - Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); - - auto tadpole = Frog::Tadpole(); - tadpole._name = unbox(julia_side_name); - - return tadpole.evolve(); -} - -template T> -Any* box(T in) -{ - auto sentinel = GCSentinel(); - static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); - - auto* out = jluna::safe_call(tadpole_ctor); - - static auto* setfield = jl_find_function("Base", "setfield!"); - static auto field_symbol = Symbol("_name"); - jluna::safe_call(setfield, out, (Any*) field_symbol, box(in._name)); - return out; -} - -template T> -Any* box(T in) -{ - auto sentinel = GCSentinel(); - static auto* frog_ctor = jl_find_function("Main", "generate_frog"); - - auto* out = jluna::safe_call(frog_ctor, box(in._name)); - return out; -} - -disable_usertype(Frog); -disable_usertype(Frog::Tadpole); - int main() { State::initialize(); - - State::safe_eval(R"( - mutable struct Tadpole - - _name::String - evolve::Function - - Tadpole() = new( - "", - (this::Tadpole) -> Frog(this._name) - ) - end - - struct Frog - - _name::String - spawn::Function - - Frog(as_tadpole::Tadpole) = new( - as_tadpole._name, - (n::Integer) -> [Tadpole() for _ in 1:n] - ) - end - )"); - - auto ted = Frog::Tadpole(); - - auto* boxed = box(ted); - Base["println"](boxed); - - ted._name = "Ted"; - auto frog = ted.evolve(); - - return 0; - Test::initialize(); Test::test("catch c exception", [](){ @@ -1041,17 +899,17 @@ int main() type = Type::construct_from>(); }); - + auto test_type = [](Type& a, T b) { - + std::string name = "Type Constant: "; name += jl_to_string((Any*) a); - + Test::test(name, [&](){ return a.operator jl_datatype_t*() == reinterpret_cast(b); }); }; - + test_type(AbstractArray_t, jl_abstractarray_type); test_type(AbstractChar_t, jl_eval_string("return AbstractChar")); test_type(AbstractFloat_t, jl_eval_string("return AbstractFloat")); @@ -1230,25 +1088,9 @@ int main() } }); - struct NonJuliaType - { - std::vector _field; - }; - - Test::test("Usertype: throw on disable", [](){ - - Test::assert_that_throws>([](){ - volatile auto* res = box(NonJuliaType()); - }); - - Test::assert_that_throws>([](){ - volatile NonJuliaType t = unbox(jl_nothing); - }); - }); - Test::test("Usertype: enable", [](){ - Usertype::enable("NonJuliaType"); + Test::assert_that(Usertype::get_name() == "NonJuliaType"); Test::assert_that(Usertype::is_enabled()); }); diff --git a/docs/frog_tadpole_example.cpp b/docs/frog_tadpole_example.cpp new file mode 100644 index 0000000..567fab0 --- /dev/null +++ b/docs/frog_tadpole_example.cpp @@ -0,0 +1,153 @@ +// +// This file compiles the entire code of the section on manual implementation of usertypes in docs/manual.md +// + +#include + +/// cpp-side implementation of frogs and tadpoles +class Frog +{ + public: + struct Tadpole + { + std::string _name; + + Tadpole() + : _name("") + {} + + Frog evolve() const + { + if (_name == "") + throw std::invalid_argument("tadpole needs to be named before evolving"); + + return Frog(_name); + } + }; + + public: + std::vector spawn(size_t n) const + { + std::vector out; + for (size_t i = 0; i < n; ++i) + out.push_back(Frog::Tadpole()); + + return out; + } + + std::string get_name() + { + return _name; + } + + private: + Frog(std::string name) + : _name(name) + {} + + std::string _name; +}; + +/// julia-side implementation of frogs and tadpoles +const char* frogs_dot_jl = R"( + mutable struct Tadpole + + _name::String + evolve::Function + + Tadpole() = new( + "", + (this::Tadpole) -> Frog(this) + ) + end + + struct Frog + + _name::String + spawn::Function + + Frog(as_tadpole::Tadpole) = new( + as_tadpole._name, + (n::Integer) -> [Tadpole() for _ in 1:n] + ) + end +)"; + +// box cpp Frog::Tadpole to julia Tadpole +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* tadpole_ctor = jl_find_function("Main", "Tadpole"); + + auto* out = jluna::safe_call(tadpole_ctor); + + static auto* setfield = jl_find_function("Base", "setfield!"); + static auto field_symbol = Symbol("_name"); + jluna::safe_call(setfield, out, (Any*) field_symbol, box(in._name)); + return out; +} + +// box cpp Frog to julia Frog +template T> +Any* box(T in) +{ + auto sentinel = GCSentinel(); + static auto* frog_ctor = jl_find_function("Main", "generate_frog"); + + auto* out = jluna::safe_call(frog_ctor, box(in._name)); + return out; +} + +// unbox julia Tadpole to cpp Frog::Tadpole +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto out = Frog::Tadpole(); + out._name = unbox(julia_side_name); + return out; +} + +// unbox julia Frog to cpp Frog +template T> +T unbox(Any* in) +{ + auto sentinel = GCSentinel(); + static auto* getfield = jl_find_function("Base", "getfield"); + static auto field_symbol = Symbol("_name"); + + Any* julia_side_name = jluna::safe_call(getfield, in, (Any*) field_symbol); + + auto tadpole = Frog::Tadpole(); + tadpole._name = unbox(julia_side_name); + + return tadpole.evolve(); +} + +int main() +{ + State::initialize(); + + State::safe_eval(frogs_dot_jl); + + auto cpp_tadpole = Frog::Tadpole(); + cpp_tadpole._name = "Ted"; + + State::new_named_undef("jl_tadpole") = box(cpp_tadpole); + State::safe_eval(R"( + println(jl_tadpole) + jl_frog = jl_tadpole.evolve(jl_tadpole); + println(jl_frog) + )"); + + Frog cpp_frog = Main["jl_frog"]; + std::cout << cpp_frog.get_name(); + + return 0; +} diff --git a/docs/manual.md b/docs/manual.md index 85ac6a3..4c4ac19 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1860,6 +1860,10 @@ struct RGBA float _green; float _blue; float _alpha; + + RGBA(float r, float g, float b) + : _red(r), _green(g), _blue(b), _alpha(1) + {} }; ``` @@ -1872,49 +1876,41 @@ For a type `T` to be manageable by `Usertype`, it needs to be *default constr ```cpp struct RGBA { - RGBA() - : _red(0), - _green(0), - _blue(0), - _alpha(1) - {} - float _red; float _green; float _blue; float _alpha; + + RGBA(float r, float g, float b) + : _red(r), _green(g), _blue(b), _alpha(1) + {} + + RGBA() + : _red(0), _green(0), _blue(0), _alpha(1 + {} }; ``` -Furthermore, `T` cannot be already (un)boxable via other `jluna` functions except for other `Usertype`-managed classes. This means `T` can't be any type in [this list of (un)boxables](#list-of-unboxables). - #### Step 2: Enabling the Interface -By default, the statement `box(/*...*/)` and `unbox(/*...*/)` compiles for any `T`. However, if we try this with our `RGBA` class: +To make `jluna` aware that we want to use the usertype interface for `RGBA`, we need to enable it at compile time. We do so using the the macro `set_usertype_enabled(T)` (executed in non-block scope) ```cpp -auto instance = RGBA(); -auto* res = box(instance); -``` -``` -terminate called after throwing an instance of 'jluna::UsertypeNotEnabledException' - what(): [C++][Exception] usertype interface for this type was not yet enabled. Call Usertype::enable(const std::string&) to instance the interface - -signal (6): Aborted -``` -An exception is thrown. To avoid unintended behavior, `jluna` requires users to manually enable the usertype interface for any specific type. We do so using: +struct RGBA +{ + /* ... */ +}; -```cpp -Usertype::enable("RGBA"); +set_usertype_enabled(RGBA); // best called right after the declaration ``` -Where the argument is the corresponding Julia-side types name, after unboxing. Note that this type does not have to be defined at this point, we will take care of that in later steps. +This generates a call to a template meta function that sets up `Usertype` for us. Among other things, it sets the julia-side name of the type we will unbox `RGBA` into, to the same name as that of the C++-side type. Meaning, C++s `RGBA` will be boxed into a new julia-side type `Main.RGBA` (we can change wether we want it to actually be in `Main` later). Because the macro is expanded at compile time, all assertions happen at compile time too, meaning you will get an early error if the type is not compliant. #### Step 3: Adding Property Routines A *property* of a struct type is what would be called a "field" in Julia and a "member" in C++. It is any named variable of any type (including functions) that is namespaced within a struct. We'll use all three terms, "field", "member", and "property" interchangeably here. -To add a property for `RGBA`s `_red`, we use: +To add a property for `RGBA`s `_red`, we use the following function at runtime: ```cpp Usertype::add_property( @@ -1961,6 +1957,7 @@ Specifying the unboxing routine is optional. If left unspecified, no operation i Now that we know how to add fields, let's fully implement the usertype interface for `RGBA` (where previous code is reprinted here for clarity): ```cpp +// in namespace scope struct RGBA { float _red = 0; @@ -1969,8 +1966,11 @@ struct RGBA float _alpha = 1; // is default constructable because all members are }; +set_usertype_enabled(RGBA); + +// ### -Usertype::enable("RGBA"); +// in function scope, i.e. inside main Usertype::add_property( "_red", [](RGBA& in) -> float {return in._red;}, @@ -2006,17 +2006,18 @@ Usertype::add_property( ); ``` -We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign for C++-side instaces. +We leave the unboxing routine for `_value` unspecified, because there is no field called `_value` to assign to for C++-side instances. #### Step 4: Implementing the Type Having added all properties to the usertype interface, we make Julia aware of the interface by calling: ```cpp +// in main Usertype::implement(); ``` -This creates a new type julia-side that has the architecture we just gave it. For end-users, this happens behind the scene, however, internally, the following expression is assembled and evaluated: +This creates a new type julia-side type that has the architecture we just gave it. For end-users, this happens behind the scene, however, internally, the following expression is assembled and evaluated: ```julia mutable struct RGBA @@ -2029,9 +2030,9 @@ mutable struct RGBA end ``` -We see that `jluna` assembled a struct type, whose field names and types are as we have specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for all the field are taken from an unmodified, default initialized instance of `T` (`RGBA()` in our case). This is why the type needs to be default constructible. We furthermore note that the julia-side type is `mutable`. +We see that `jluna` assembled a mutable struct type, whose field names and types are as specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for the types field are taken from an unmodified, default initialized instance of `T` (`RGBA()` in our case). This is why the type needs to be default constructible. -Having evaluated the above expression, we fully implemented the usertype interface for `RGBA`. From this point onwards, the following works: +Having evaluated the above expression, we fully implemented the usertype interface for `RGBA`! From this point onwards, the following works: ```cpp auto instance = RGBA(); @@ -2054,12 +2055,11 @@ RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) 0.5 ``` -Because `box` and `unbox` are now defined, all of `jluna`s other functionality now also works with `RGBA`. This includes assigning proxies, casting proxies to `RGBA`, calling julia-side functions with C++-side arguments, even using `RGBA` as value types for a `jluna` array, etc. +Because `box` and `unbox` are now defined, all of `jluna`s other functionality now also works with `RGBA`. This includes assigning proxies, casting proxies to `RGBA`, calling julia-side functions with C++-side arguments, even using `RGBA` as value types for a `jluna` arrays, etc. ### Manual Implementation -`jluna::Usertype` works great if we have a C++-side type that we want to move to julia because it creates a completely new type from the C++-type. However, what if we already have an existing julia type and we want to map an arbitrary C++ type to it? We could, of course, box the C++-type to the type `Usertype` created, then convert that type to our julia type. While this is a decent option for most users, it is suboptimal performance wise. In cases like these, we will have to resort to manually implementing box/unbox. This section will work through an example, giving users a template on how to achieve this. - +`jluna::Usertype` works great if we already have a C++-side type that we want to julia because it creates a new type for us and automates the mapping of fields from C++ to Julia. What, however, if we already have a Julia-side type? In cases like this, we will have to resort to manually implementing `box`/`unbox` without the help of `Usertype`. This section will work through an example, providing templates for users to modify according to their own specifications. #### Example: Tadpoles & Frogs @@ -2152,7 +2152,7 @@ struct Frog # immutable because _name only has getter end ``` -Where we defined the frogs ctor as only being constructable from a tadpole, which emulates the behavior the classes exhibit C++ side. A bit is lost in translation here as julia usually discourages member functions in the C++ sense, but for the sake of the example we defined them here using the following equivalence: +Where we defined the frogs ctor as only being constructable from a tadpole, which emulates the behavior the classes exhibit C++ side. A bit of functionality is lost in translation here, as julia usually discourages member functions in the C++ sense, but for the sake of the example we defined them here using the following equivalence: ```julia # in cpp: @@ -2166,7 +2166,7 @@ InstanceType.member_function(instance, arguments) By making the instance the argument, julia functions inside a struct emulate the C++ behavior of being able to access all members of the struct they are part of. -### Boxing Definition +### Function Definition We now want to implement boxing and unboxing for both frogs and tadpoles. When defining these functions for a custom type, they need to adhere to the following signatures exactly: @@ -2178,23 +2178,27 @@ template T> T unbox(Any*); ``` -Where `Is` is a concept that enforces `T` to be equal to `U`. We need to use concepts and template functions here, so the syntax `box(/*...*/)` and `unbox(/*...*/)` are valid, which is required by most of `jluna`s functions. Given this signature, the `box` definitions for `Frog` and `Frog::Tadpole` look like this: +Where `Is` is a concept that enforces `T` to be equal to `U`. We need to use concepts and template functions here, so the syntax `box(/*...*/)` and `unbox(/*...*/)` are valid, which is specifically required by most of `jluna`s functions. The `box` and `unbox` definitions for `Frog` and `Frog::Tadpole` look like this: ```cpp template T> Any* box(T in) -{ - -} +{} template T> +Any* box(T in) +{} + +template T> T unbox(Any* in) -{ - -} +{} + +template T> +T unbox(Any* in) +{} ``` -Because we are handling raw C-pointers and not proxies, we need to manually protect ourself from the garbage collector (GC). This is done best using `jluna::GCSentinel`, which is a class that disables the GC while it is in scope: +Turning our attention to `box`, specifically, for a while: Because we are handling raw C-pointers and not proxies, we need to manually protect ourself from the garbage collector (GC). This is done best, by using `jluna::GCSentinel`, which is a class that disables the GC while it is in scope: ```cpp template T> @@ -2204,7 +2208,7 @@ Any* box(T in) } template T> -T unbox(Any* in) +Any* box(T in) { auto sentinel = GCSentinel(); } @@ -2295,9 +2299,7 @@ Any* box(T in) Where we used `jluna::Symbol` to what needs to be `:_name` julia-side. We specified it as `static` as that creation only needs to happen once over the course of the runtime, saving a little bit of overhead each call. -#### Implement unbox - -Now that we have box fully written, we can turn our attention to unbox. Unboxing is much simpler, all we need to do is access the property of the julia-side instance pointed to by the argument using `Base.getfield`, then construct the C++-side instance. Because we are back C++-side, we can use `Tadpole::evolve` to create a frog, as intended: +Now that we have box fully written, we can turn our attention to unbox. Unboxing is much simpler, all we need to do, is to access the property of the julia-side instance pointed to by the argument, using `Base.getfield`, then construct the C++-side instance. Because we are back C++-side, we can use `Tadpole::evolve` to create a frog, as intended: ```cpp template T> @@ -2331,11 +2333,25 @@ T unbox(Any* in) ``` Where we again allocate the julia-side symbol `:_name` statically to save on allocation per box call. +Because we implement box/unbox, both `Frog` and `Frog::Tadpole` are now compliant and available to all of `jluna`s functionality: +```cpp +auto cpp_tadpole = Frog::Tadpole(); +cpp_tadpole._name = "Ted"; +State::new_named_undef("jl_tadpole") = box(tadpole); +State::safe_eval("jl_frog = Tadpole.evolve(jl_tadpole)"); +auto cpp_frog = Main["jl_frog"]; +std::cout << cpp_frog.get_name(); +``` +``` +Tadpole("Ted", var"#1#2"()) +Frog("Ted", var"#3#5"()) +Ted +``` - +The full code for this section is available [here](./frog_tadpole_example.cpp). --- @@ -2437,6 +2453,8 @@ All performance critical code should be inside a Julia file. All C++ should do, By far the easiest way to completely tank a programs' performance is to unnecessarily box/unbox values constantly. If our program requires operation `A`, `B`, `C` julia-side and `X`, `Y`, `Z` C++-side, it's very important to execute everything in a way that minimizes the number of box/unbox calls. So: `ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls, because of this it sometimes encouraged to not shy away from handling pure `Any*`, just make sure to also manually control the garbage collector. +Lastly, though it is cumbersome, manually implementing box/unbox calls instead of going through `Usertype` often gives more freedom to optimize, as special julia-side functions that increase performance can be called manually, instead of through the (un)boxing routine lambdas. Advanced users are therefore encouraged to consider this method frequently. + ### Minimize Proxy Construction The main overhead incurred by `jluna` is that of safety. Anytime a proxy is constructed, its value and name have to be added to an internal state that protects them from the garbage collector. This takes some amount of time; internally, the value is wrapped and a reference to it and its name is stored in a dictionary. Neither of these is that expensive but when a function uses hundreds of proxies over its runtime, this can add up quickly. Consider the following: diff --git a/include/concepts.hpp b/include/concepts.hpp index bdf0924..78f3514 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -133,33 +133,12 @@ namespace jluna /// @concept should be resolved as usertype template - struct usertype_disabled + struct usertype_enabled { + constexpr static inline const char* name = ""; constexpr static inline bool value = false; }; template - concept HasUsertypeInterfaceDisabled = usertype_disabled::value; - - #define disable_usertype(T) template<> struct jluna::usertype_disabled {constexpr static inline bool value = true;}; - - /// @concept is none of the above and default constructible - template - concept IsUsertype = - not IsJuliaValuePointer - and not IsAnyPtrCastable - and not IsPrimitive - and not IsComplex - and not IsVector - and not IsSet - and not IsMap - and not IsPair - and not IsTuple - and not LambdaType - and not LambdaType - and not LambdaType - and not LambdaType - and not LambdaType - and not LambdaType> - and not HasUsertypeInterfaceDisabled; + concept IsUsertype = usertype_enabled::value; } \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 2cc7019..9d34edd 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -12,6 +12,10 @@ namespace jluna { + /// @brief declare T to be a usertype at compile time, uses C++-side name as Julia-side typename + /// @param T: type + #define set_usertype_enabled(T) template<> struct jluna::usertype_enabled {static_assert(IsDefaultConstructible, "types managed by Usertype need to be default constructable"); constexpr static inline const char* name = #T; constexpr static inline bool value = true;}; + /// @brief customizable wrapper for non-julia type T /// @note for information on how to use this class, visit https://github.com/Clemapfel/jluna/blob/master/docs/manual.md#usertypes template @@ -27,14 +31,14 @@ namespace jluna /// @brief ctor delete, static-only interface Usertype() = delete; - /// @brief enable interface - /// @param name - static void enable(const std::string&); - - /// @brief is enabled + /// @brief was enabled at compile time using set_usertype_enabled /// @returns bool static bool is_enabled(); + /// @brief get julia-side name + /// @returns name + static std::string get_name(); + /// @brief add field /// @param name: julia-side name of field /// @param box_get: lambda with signature (T&) -> Any* @@ -67,7 +71,7 @@ namespace jluna static T unbox(Any*); private: - static inline bool _enabled = false; + static void initialize(); static inline bool _implemented = false; static inline std::unique_ptr _type = std::unique_ptr(nullptr); @@ -80,22 +84,6 @@ namespace jluna Type >> _mapping = {}; }; - - /// @brief exception thrown when usertype is used before being implemented - template - struct UsertypeNotEnabledException : public std::exception - { - public: - /// @brief ctor - UsertypeNotEnabledException(); - - /// @brief what - /// @returns message - const char* what() const noexcept override final; - - private: - std::string _msg; - }; } #include ".src/usertype.inl" \ No newline at end of file From e1ba8f05d2d2ada32906b6da2df4941a815b28a0 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 19:08:17 +0100 Subject: [PATCH 55/56] polish --- docs/frog_tadpole_example.cpp | 8 ++- docs/manual.md | 107 ++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/docs/frog_tadpole_example.cpp b/docs/frog_tadpole_example.cpp index 567fab0..9ffd067 100644 --- a/docs/frog_tadpole_example.cpp +++ b/docs/frog_tadpole_example.cpp @@ -1,5 +1,5 @@ // -// This file compiles the entire code of the section on manual implementation of usertypes in docs/manual.md +// This file contains the code of the section on manual implementation of usertypes in docs/manual.md // #include @@ -71,6 +71,12 @@ const char* frogs_dot_jl = R"( (n::Integer) -> [Tadpole() for _ in 1:n] ) end + + function generate_frog(name::String) ::Frog + tadpole = Tadpole() + tadpole._name = name + return Frog(tadpole) + end )"; // box cpp Frog::Tadpole to julia Tadpole diff --git a/docs/manual.md b/docs/manual.md index 4c4ac19..a5a6191 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -1867,11 +1867,11 @@ struct RGBA }; ``` -While it may be possible to manually translate this class into a Julia-side `NamedTuple`, this is rarely the best option. For more complex classes, this is often not possible at all. To make classes like this (un)boxable, we use `jluna::Usertype`. +While it may be possible to manually translate this class into a Julia-side `NamedTuple`, this is rarely the best option. For more complex classes, this is often not possible at all. To make classes like this (un)boxable, we use `jluna::Usertype`, the usertype interface. #### Step 1: Making the Type compliant -For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no explicit default constructor, so we should add it: +For a type `T` to be manageable by `Usertype`, it needs to be *default constructable*. `RGBA` currently has no default constructor, so we need to add it: ```cpp struct RGBA @@ -1891,9 +1891,11 @@ struct RGBA }; ``` +If the type `T` is not default constructable, a static assertion is triggered at compile time. + #### Step 2: Enabling the Interface -To make `jluna` aware that we want to use the usertype interface for `RGBA`, we need to enable it at compile time. We do so using the the macro `set_usertype_enabled(T)` (executed in non-block scope) +To make `jluna` aware that we want to use the usertype interface for `RGBA`, we need to **enable it** at compile time. To do this, we use the macro `set_usertype_enabled(T)` (executed in non-block scope). ```cpp struct RGBA @@ -1904,11 +1906,11 @@ struct RGBA set_usertype_enabled(RGBA); // best called right after the declaration ``` -This generates a call to a template meta function that sets up `Usertype` for us. Among other things, it sets the julia-side name of the type we will unbox `RGBA` into, to the same name as that of the C++-side type. Meaning, C++s `RGBA` will be boxed into a new julia-side type `Main.RGBA` (we can change wether we want it to actually be in `Main` later). Because the macro is expanded at compile time, all assertions happen at compile time too, meaning you will get an early error if the type is not compliant. +This generates a call to a template meta function that sets up `Usertype` for us. Among other things, it sets the julia-side name of the type we will unbox `RGBA` into, to the same name as that of the C++-side type. Meaning, C++s `RGBA` will be boxed into a new julia-side type called `Main.RGBA`. We can change whether we want it to actually be in `Main` scope later on. #### Step 3: Adding Property Routines -A *property* of a struct type is what would be called a "field" in Julia and a "member" in C++. It is any named variable of any type (including functions) that is namespaced within a struct. We'll use all three terms, "field", "member", and "property" interchangeably here. +A *property* of a struct type is what would be called a "field" in Julia or a "member" in C++. It is any named variable of any type (including functions) that is namespaced within a struct. We'll use all three terms, "field", "member", and "property" interchangeably here. To add a property for `RGBA`s `_red`, we use the following function at runtime: @@ -1940,9 +1942,9 @@ Any* boxed = box(instance); // then assigns boxed._red with result ``` -The result of the boxing routine lambda is assigned to the field of the specified name of the resulting Julia type when `box` is called. +When `box` is called, the result of the boxing routine lambda is assigned to the field of the specified name. -The third argument is the **unboxing routine**. This lambda has the signature `(T&, Property_t) -> void` which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the cpp-side instance that is the result of the `unbox` call, the second argument is the value of the field of the corresponding name of the julia-side instance. +The third argument is the **unboxing routine**. This lambda has the signature `(T&, Property_t) -> void`, which in this case becomes `(RGBA&, float) -> void`. The unboxing routine is called during unboxing, its first argument is the cpp-side instance that is the result of the `unbox` call, the second argument is the value of the field of the corresponding name of the julia-side instance. ```cpp auto instance = RGBA(); @@ -1952,7 +1954,11 @@ RGBA unboxed = unbox(boxed); // calls [](RGBA& out, float in) with out = unboxed, in = unboxed._red ``` -Specifying the unboxing routine is optional. If left unspecified, no operation is performed during unboxing. +Specifying the unboxing routine is optional. If left unspecified, no operation is performed during unboxing. The boxing routine is not optional, if we want it to always return a constant value, we can simple create a lambda for this like so: + +```cpp +Usertype::add_field("_constant_field", [](T& _) -> float {return 1;}); +``` Now that we know how to add fields, let's fully implement the usertype interface for `RGBA` (where previous code is reprinted here for clarity): @@ -1960,11 +1966,18 @@ Now that we know how to add fields, let's fully implement the usertype interface // in namespace scope struct RGBA { - float _red = 0; - float _green = 0; - float _blue = 0; - float _alpha = 1; - // is default constructable because all members are + float _red; + float _green; + float _blue; + float _alpha; + + RGBA(float r, float g, float b) + : _red(r), _green(g), _blue(b), _alpha(1) + {} + + RGBA() + : _red(0), _green(0), _blue(0), _alpha(1 + {} }; set_usertype_enabled(RGBA); @@ -1999,7 +2012,7 @@ Usertype::add_property( "_value", [](RGBA& in) -> float { float max = 0; - for (v : {in._red, in._green, in._blue}) + for (auto v : {in._red, in._green, in._blue}) max = std::max(v, max); return max; } @@ -2010,7 +2023,7 @@ We leave the unboxing routine for `_value` unspecified, because there is no fiel #### Step 4: Implementing the Type -Having added all properties to the usertype interface, we make Julia aware of the interface by calling: +Having added all properties to the usertype interface, we make the Julia state aware of the interface by calling: ```cpp // in main @@ -2030,9 +2043,9 @@ mutable struct RGBA end ``` -We see that `jluna` assembled a mutable struct type, whose field names and types are as specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for the types field are taken from an unmodified, default initialized instance of `T` (`RGBA()` in our case). This is why the type needs to be default constructible. +We see that `jluna` assembled a mutable struct type, whose field names and types are as specified. Even the order in which we called `add_field` for specific names is preserved. This becomes important for the default constructor (a constructor that takes no arguments). The default values for each of the types fields are those of an unmodified, default initialized instance of `T` (`RGBA()` in our case). This is why the type needs to be default constructable. -Having evaluated the above expression, we fully implemented the usertype interface for `RGBA`! From this point onwards, the following works: +Having evaluated the above expression, we have fully implemented the usertype interface for `RGBA`. From this point onwards, the following works: ```cpp auto instance = RGBA(); @@ -2055,15 +2068,17 @@ RGBA(1.0f0, 0.0f0, 1.0f0, 1.0f0, 1.0f0) 0.5 ``` -Because `box` and `unbox` are now defined, all of `jluna`s other functionality now also works with `RGBA`. This includes assigning proxies, casting proxies to `RGBA`, calling julia-side functions with C++-side arguments, even using `RGBA` as value types for a `jluna` arrays, etc. +Because `box` and `unbox` are now defined and there is a corresponding julia-side type for `RGBA`, all of `jluna`s other functionality is now also works with `RGBA`. This includes assigning proxies, casting proxies to `RGBA`, calling julia-side functions with C++-side arguments, even using `RGBA` as value types for a `jluna` arrays, etc. ### Manual Implementation -`jluna::Usertype` works great if we already have a C++-side type that we want to julia because it creates a new type for us and automates the mapping of fields from C++ to Julia. What, however, if we already have a Julia-side type? In cases like this, we will have to resort to manually implementing `box`/`unbox` without the help of `Usertype`. This section will work through an example, providing templates for users to modify according to their own specifications. +`jluna::Usertype` works great if we already have a C++-side type that we want to move to julia. What, however, if we already have a Julia-side type and we want to map a the C++-side type onto it? In cases like this, we will have to resort to manually implementing `box`/`unbox` without the help of `Usertype`. + +In this section, we'll work through a somewhat complex example, which provides a template for users to apply to their own specific classes. #### Example: Tadpoles & Frogs -Let's first introduce our example: Consider the following classes: +Let's first introduce our problem: Consider the following classes: ```cpp class Frog @@ -2116,7 +2131,7 @@ class Frog }; ``` -These classes are deceptively simple, however they provide some unique challenges. We cannot instance a `Frog` (henceforth "frog"), the only way to construct a frog is by first creating a `Frog::Tadpole` (henceforth "tadpole"), naming it, then calling `evolve`. Because tadpole is an internal class of `Frog`, it has access to the private constructor. Furthermore, while we can access a frogs name, the name cannot be changed. The name of a tadpole can be both access and changed. +These classes are deceptively simple, however they provide some unique challenges. We cannot instance a `Frog` (henceforth "frog"), the only way to construct a frog is by first creating a `Frog::Tadpole` (henceforth "tadpole"), naming it, then calling `evolve`. Because tadpole is an internal class of `Frog`, it has access to the private constructor. Furthermore, while we can *access* a frogs name, we cannot change it. The name of a tadpole can be both access and changed. Let's say we want to translate this functionality to Julia without using `Usertype`. The julia-versions of the above could be: @@ -2152,7 +2167,7 @@ struct Frog # immutable because _name only has getter end ``` -Where we defined the frogs ctor as only being constructable from a tadpole, which emulates the behavior the classes exhibit C++ side. A bit of functionality is lost in translation here, as julia usually discourages member functions in the C++ sense, but for the sake of the example we defined them here using the following equivalence: +Where we defined the frogs ctor as only being constructable from a tadpole. This emulates the behavior the classes exhibit C++ side. Julia usually discourages member functions in the C++ sense, but for the sake of this example, we defined them using the following equivalence: ```julia # in cpp: @@ -2161,10 +2176,10 @@ instance.member_function(arguments); # in julia instance = InstanceType() -InstanceType.member_function(instance, arguments) +instance.member_function(instance, arguments) ``` -By making the instance the argument, julia functions inside a struct emulate the C++ behavior of being able to access all members of the struct they are part of. +By making the instance an argument, the julia function emulate the C++ behavior of being able to access all members of the struct they are part of. ### Function Definition @@ -2178,7 +2193,7 @@ template T> T unbox(Any*); ``` -Where `Is` is a concept that enforces `T` to be equal to `U`. We need to use concepts and template functions here, so the syntax `box(/*...*/)` and `unbox(/*...*/)` are valid, which is specifically required by most of `jluna`s functions. The `box` and `unbox` definitions for `Frog` and `Frog::Tadpole` look like this: +Where `Is` is a concept that enforces `T` to be equal to `U`. We need to use concepts and template functions here, so the syntax `box(/*...*/)` and `unbox(/*...*/)` are valid, which is required by `jluna`s functions. The `box` and `unbox` signatures for `Frog` and `Frog::Tadpole` then look like this: ```cpp template T> @@ -2198,7 +2213,7 @@ T unbox(Any* in) {} ``` -Turning our attention to `box`, specifically, for a while: Because we are handling raw C-pointers and not proxies, we need to manually protect ourself from the garbage collector (GC). This is done best, by using `jluna::GCSentinel`, which is a class that disables the GC while it is in scope: +Because we are handling raw C-pointers and not proxies, we need to manually protect ourself from the garbage collector (GC). This is done best, by using `jluna::GCSentinel`, which is a class that disables the GC while it is in scope: ```cpp template T> @@ -2214,7 +2229,7 @@ Any* box(T in) } ``` -To create the julia-side versions of the classes, we need access to their julia-side constructors. We can do so using `jl_find_function`, then using the constructors to create instances: +To instance the julia-side versions of the classes, we need access to their julia-side constructors. We can do so using `jl_find_function`, using the result like a function to create instances: ```cpp template T> @@ -2236,7 +2251,7 @@ Any* box(T in) } ``` -But we run into a problem, `Frog` does not have a no-argument CTOR. The only way to construct frog is from a tadpole, however we cannot ask for a tadpole as an argument to the box function because we have to strictly adhere to the `(T) -> Any*` signature. The only way to solve this conundrum is to create a new julia-side function that constructors a frog for us in a round-about way: +We run into a problem. `Frog` does not have a no-argument constructor. The only way to construct a frog, is from a tadpole. We cannot ask for a tadpole as an argument to the box function, though, because we have to strictly adhere to the `(T) -> Any*` signature. The only way to solve this conundrum, is to create a new julia-side function that constructs a frog for us, in a round-about way: ```julia function generate_frog(name::String) ::Frog @@ -2245,7 +2260,9 @@ function generate_frog(name::String) ::Frog return Frog(tadpole) end ``` -The example in this section was picked this way, specifically to illustrate how, when the julia-type is pre-defined, we will often do some extra work to make C++ and Julia play nice together. We cannot extend the julia-side `Frog` class, which is why we need to create this wrapper function to construct our frog instead. Accessing and calling the function inside `box>` now looks like this: +The example in this section was specifically setup this way, to illustrate how, when the julia-type is pre-defined and cannot be extended, we will often do extra work to make C++ and Julia play nice together. We cannot extend the julia-side `Frog` class, which is why we need to create this wrapper function to construct a frog instead. + +Accessing and calling the function inside `box>`: ```cpp template T> @@ -2267,9 +2284,9 @@ Any* box(T in) } ``` -Where we also had to manually box the string, because we are not dealing with proxies which do this automatically anymore. +Where we also had to manually box the string, because we are not dealing with proxies, which do this automatically, anymore. -Lastly, we need to update the newly created julia-side instance with the actual values of the C++-side instance, otherwise all `box` would be is a way to construct new frogs and tadpoles. To do this, we use `Base.setfield!`. Note, however, that `Frog` julia-side is immutable. This is why we created the generator function, the only way to make julia-side frogs have the same name as C++-side frogs, is to assign their name on construction. Because of this, the frog instanced in `box` does not need to be modified further: +Lastly, we need to update the newly created julia-side instance with the actual values of the C++-side instance. To do this, we use `Base.setfield!`. Note, however, that the julia-side `Frog` is immutable. This is why we created the generator function, the only way to make julia-side frogs have the same name as C++-side frogs, is to assign their name to a tadpole, then evolving it. Because of this, the frog instanced in `box` does not need to be modified further: ```cpp template T> @@ -2297,9 +2314,9 @@ Any* box(T in) } ``` -Where we used `jluna::Symbol` to what needs to be `:_name` julia-side. We specified it as `static` as that creation only needs to happen once over the course of the runtime, saving a little bit of overhead each call. +Where we used `jluna::Symbol` create a symbol `:_name`, julia-side. We used the `static` keyword, because the value of this symbol will never change, thus only initializing it once saves runtime on further calls to `box`. -Now that we have box fully written, we can turn our attention to unbox. Unboxing is much simpler, all we need to do, is to access the property of the julia-side instance pointed to by the argument, using `Base.getfield`, then construct the C++-side instance. Because we are back C++-side, we can use `Tadpole::evolve` to create a frog, as intended: +Now that we have box fully written, we can turn our attention to unbox. Unboxing is much simpler, all we need to do, is to access the properties of the julia-side instances using `Base.getfield`. Because we are now back C++-side, we use `Tadpole::evolve` to create a frog: ```cpp template T> @@ -2332,8 +2349,9 @@ T unbox(Any* in) } ``` -Where we again allocate the julia-side symbol `:_name` statically to save on allocation per box call. -Because we implement box/unbox, both `Frog` and `Frog::Tadpole` are now compliant and available to all of `jluna`s functionality: +Where we allocate the symbol `:_name`, as before. + +Because both box and unbox are now implemented for `Frog` and `Frog::Tadpole`, both are now compliant, and can be freely transferred between states to be used by all of `jluna`s functions: ```cpp auto cpp_tadpole = Frog::Tadpole(); @@ -2351,17 +2369,17 @@ Frog("Ted", var"#3#5"()) Ted ``` -The full code for this section is available [here](./frog_tadpole_example.cpp). +The complete code for this example is available [here](./frog_tadpole_example.cpp). --- ## Performance -This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library, though, is a much fairer comparison and in that aspect it can do quite well. It is still important to be aware of how to avoid overhead, and when it may be worth it to stay with the C-library. +This section will give some tips on how to achieve the best performance using `jluna`. As of release 0.7, `jluna` went through extensive optimization, minimizing overhead as much as possible. Still, when compared to pure julia, `jluna` will always be slower. Comparing `jluna` to the C-library, though, is a much fairer comparison and in that aspect, it can do quite well. It is still important to be aware of how to avoid overhead, and when it may be worth it to resort to only using the C-library. ### Cheat Sheet -Here, we provide a grading of most of `jluna`s features in terms of runtime performance, where `A+` means there is literally 0-overhead, `F` means "do not use this in performance critical code": +Provided here is a grading of most of `jluna`s features in terms of runtime performance, where `A+` means there is literally 0-overhead compared to operating purely in julia, `F` means "do not use this in performance critical code": ```cpp // function or feature // grade @@ -2427,7 +2445,7 @@ The following suggestions are good to keep in mind when writing performance-crit ### Avoid String Parsing -It's easy to fall into treating `jluna` like the REPL: a tool to control Julia through the use of commands supplied as strings. This is rarely the most optimal way to trigger Julia-side actions, however. Notably, when calling Julia functions, often the following pattern, of calling functions directly through C, achieves much better performance: +It's easy to fall into the pattern of treating `jluna` like the REPL: a way to control Julia through the use of commands *supplied as strings*. This is rarely the most optimal way to trigger Julia-side actions, however. Notably, when calling Julia functions, often, the following pattern of calling functions directly through C achieves much better performance: ```cpp // 1) exactly as fast as pure julia @@ -2446,18 +2464,19 @@ Where `static` was used, so the `call_function` pointer is only assigned exactly ### Stay Julia-Side -All performance critical code should be inside a Julia file. All C++ should do, is to manage data and dispatch the Julia-side functions. For example, let's say we are working on very big matrices. Any matrix operation should happen inside a julia function, the actual data of the matrix should stay julia-side as much as possible. We can still control julia from C++, `State::eval` has almost no overhead. Similarly, `jluna::Proxy` is just a stand-in for julia-side data. Thus, using julia functions of `jluna::Proxy`s incurs very little overhead compared to doing both only in julia. +All performance critical code should be inside a Julia file. All C++ should do, is to manage data and dispatch the Julia-side functions. For example, let's say we are working on very big matrices. Any matrix operation should happen inside a julia function, the actual data of the matrix should stay julia-side as much as possible. + Relating to this, it is important to realize, when a C++ object does not actually manage any data C++-side. For example, `jluna::Proxy` is just a stand-in for julia-side object. The C++-part of it basically only holds a pointer to the data. Thus, using julia functions on `jluna::Proxy`s incurs very little overhead, compared to doing both only in julia, because all we are doing is using julia-side functions on julia-side objects. ### Minimize (Un)Boxing By far the easiest way to completely tank a programs' performance is to unnecessarily box/unbox values constantly. If our program requires operation `A`, `B`, `C` julia-side and `X`, `Y`, `Z` C++-side, it's very important to execute everything in a way that minimizes the number of box/unbox calls. So: -`ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls, because of this it sometimes encouraged to not shy away from handling pure `Any*`, just make sure to also manually control the garbage collector. +`ABC XYZ ` rather than `A X B Y `, etc.. This may seem obvious when abstracted like this, but in a complex application, it's easy to forget. Knowing what calls cause box/unboxing is essential to avoiding pitfalls, because of this it sometimes encouraged to not shy away from handling pure `Any*`, just make sure to also manually control the garbage collector. -Lastly, though it is cumbersome, manually implementing box/unbox calls instead of going through `Usertype` often gives more freedom to optimize, as special julia-side functions that increase performance can be called manually, instead of through the (un)boxing routine lambdas. Advanced users are therefore encouraged to consider this method frequently. +Lastly, though it is cumbersome, manually implementing box/unbox calls instead of going through `Usertype` often gives more freedom to optimize. Advanced users are therefore encouraged to consider this method more frequently, at the cost of convenience. ### Minimize Proxy Construction -The main overhead incurred by `jluna` is that of safety. Anytime a proxy is constructed, its value and name have to be added to an internal state that protects them from the garbage collector. This takes some amount of time; internally, the value is wrapped and a reference to it and its name is stored in a dictionary. Neither of these is that expensive but when a function uses hundreds of proxies over its runtime, this can add up quickly. Consider the following: +The main overhead incurred by `jluna` is that of safety. Anytime a proxy is constructed, its value and name have to be added to an internal state that protects them from the garbage collector. This takes some amount of time; internally, the value is wrapped and a reference to it and its name is stored in a dictionary. Neither of these is that expensive, but when a function uses hundreds of proxies over its runtime, this can add up quickly. Consider the following: ```cpp auto a = Main["Module1"]["Module2"]["vector_var"][1]["field"]; @@ -2491,7 +2510,7 @@ We do not create any proxy at all, increasing performance by up to 6 times. Of c auto a_proxy = jluna::Proxy(jluna::safe_eval("Main.Module1.Module2.vector_var[1].field")); size_t a_value = a_proxy; ``` -This calls the proxy constructor with a pure Any* returned through `safe_eval`, reducing the number of proxies from 6 to 1. +This calls the proxy constructor using a pure `Any*`, returned through `safe_eval`, thereby reducing the number of proxies constructed from 6 named to only 1 unnamed. ### Use the C-Library From 3c8266d16b37f4877bf5c730796dedb063c46d82 Mon Sep 17 00:00:00 2001 From: clem Date: Sun, 6 Mar 2022 19:34:51 +0100 Subject: [PATCH 56/56] all compilers working --- .test/main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.test/main.cpp b/.test/main.cpp index 63657e1..1ec7afa 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -11,6 +11,12 @@ using namespace jluna; using namespace jluna::detail; +struct NonJuliaType +{ + std::vector _field; +}; +set_usertype_enabled(NonJuliaType); + int main() { State::initialize();