diff --git a/CMakeLists.txt b/CMakeLists.txt index a3c39ec..972ee6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ set(JLCXX_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src) set(JLCXX_HEADERS ${JLCXX_INCLUDE_DIR}/jlcxx/array.hpp + ${JLCXX_INCLUDE_DIR}/jlcxx/attr.hpp ${JLCXX_INCLUDE_DIR}/jlcxx/const_array.hpp ${JLCXX_INCLUDE_DIR}/jlcxx/jlcxx.hpp ${JLCXX_INCLUDE_DIR}/jlcxx/jlcxx_config.hpp diff --git a/examples/functions.cpp b/examples/functions.cpp index ebf4824..8d1094f 100644 --- a/examples/functions.cpp +++ b/examples/functions.cpp @@ -202,12 +202,19 @@ JLCXX_MODULE init_test_module(jlcxx::Module& mod) mod.method("boxednumber_nb_deleted", [] () { return BoxedNumber::m_nb_deleted; }); mod.method("concatenate_numbers", &concatenate_numbers); + mod.method("concatenate_numbers_with_named_args", &concatenate_numbers, jlcxx::arg("i"), jlcxx::arg("d")); + // wrong number of arguments doesn't compile + // mod.method("concatenate_numbers_with_kwargs", &concatenate_numbers, jlcxx::kwarg("i")); + mod.method("concatenate_numbers_with_kwargs", &concatenate_numbers, jlcxx::kwarg("i"), jlcxx::kwarg("d")); + mod.method("concatenate_numbers_with_default_values", &concatenate_numbers, jlcxx::arg("i"), jlcxx::arg("d")=5.2); + mod.method("concatenate_numbers_with_default_values_of_different_type", &concatenate_numbers, jlcxx::arg("i"), jlcxx::arg("d")=5); + mod.method("concatenate_numbers_with_default_kwarg", &concatenate_numbers, jlcxx::arg("i"), jlcxx::kwarg("d")=5.2); mod.method("concatenate_strings", &concatenate_strings); mod.method("test_int32_array", test_int32_array); mod.method("test_int64_array", test_int64_array); mod.method("test_float_array", test_float_array); mod.method("test_double_array", test_double_array); - mod.method("test_exception", test_exception, "", true); + mod.method("test_exception", test_exception, jlcxx::calling_policy::std_function); mod.method("test_array_len", test_array_len); mod.method("test_array_set", test_array_set); mod.method("test_array_get", test_array_get); diff --git a/examples/types.cpp b/examples/types.cpp index 68c3b6f..72d1d6e 100644 --- a/examples/types.cpp +++ b/examples/types.cpp @@ -231,7 +231,7 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& types) types.add_type("World") .constructor() - .constructor(false) // no finalizer + .constructor(jlcxx::finalize_policy::no) // no finalizer .constructor([] (const std::string& a, const std::string& b) { return new World(a + " " + b); }) .method("set", &World::set) .method("greet_cref", &World::greet) diff --git a/include/jlcxx/attr.hpp b/include/jlcxx/attr.hpp new file mode 100644 index 0000000..8615a1a --- /dev/null +++ b/include/jlcxx/attr.hpp @@ -0,0 +1,194 @@ +#ifndef JLCXX_ATTR_HPP +#define JLCXX_ATTR_HPP + +#include +#include +#include +#include + +#include "jlcxx_config.hpp" +#include "type_conversion.hpp" + + +// This header provides internal helper functionality for providing additional information like argument names and default arguments for C++ functions (method in module.hpp) + +namespace jlcxx +{ + +namespace detail +{ + /// Helper type for function arguments + template + struct JLCXX_API BasicArg + { + static constexpr bool isKeywordArgument = IsKwArg; + + const char *name = nullptr; + jl_value_t* defaultValue = nullptr; + + BasicArg(const char *name_) : name(name_) {} + + template + inline BasicArg &operator=(T value) + { + defaultValue = box(std::forward(value)); + return *this; + } + }; +} + +/// use jlcxx::arg("argumentName") to add function argument names, and jlcxx::arg("name")=value to define an argument with a default value +using arg = detail::BasicArg; + +///! use jlcxx::kwarg("argumentName") to define a keyword argument and with jlcxx::kwarg("name")=value you can add a default value for the argument +using kwarg = detail::BasicArg; + +/// enum for the force_convert parameter for raw function pointers +enum class calling_policy : bool +{ + ccall = false, + std_function = true +}; +/// default value for the calling_policy argument for Module::method with raw C++ function pointers +constexpr auto default_calling_policy = calling_policy::ccall; + +/// enum for finalize parameter for constructors +enum class finalize_policy : bool +{ + no = false, + yes = true +}; +/// default value for the finalize_policy argument for Module::constructor +constexpr auto default_finalize_policy = finalize_policy::yes; + + +namespace detail +{ + /// SFINEA for argument processing, inspired/copied from pybind11 code (pybind11/attr.h) + template + struct process_attribute; + + /// helper type for parsing argument and docstrings + struct ExtraFunctionData + { + std::vector positionalArguments; + std::vector keywordArguments; + std::string doc; + calling_policy force_convert = default_calling_policy; + finalize_policy finalize = default_finalize_policy; + + }; + + /// process docstring + template<> + struct process_attribute + { + static inline void init(const char* s, ExtraFunctionData& f) + { + f.doc = s; + } + }; + + template<> + struct process_attribute : public process_attribute {}; + + /// process positional argument + template<> + struct process_attribute + { + static inline void init(arg&& a, ExtraFunctionData& f) + { + f.positionalArguments.emplace_back(std::move(a)); + } + }; + + /// process keyword argument + template<> + struct process_attribute + { + static inline void init(kwarg&& a, ExtraFunctionData& f) + { + f.keywordArguments.emplace_back(std::move(a)); + } + }; + + /// process calling_policy argument + template<> + struct process_attribute + { + static inline void init(calling_policy force_convert, ExtraFunctionData& f) + { + f.force_convert = force_convert; + } + }; + + /// process finalize_policy argument + template<> + struct process_attribute + { + static inline void init(finalize_policy finalize, ExtraFunctionData& f) + { + f.finalize = finalize; + } + }; + + template + void parse_attributes_helper(ExtraFunctionData& f, T argi) + { + using T_ = typename std::decay_t; + process_attribute::init(std::forward(argi), f); + } + + /// initialize ExtraFunctionData from argument list + template + ExtraFunctionData parse_attributes(Extra... extra) + { + // check that the calling_policy is only set if explicitly allowed + constexpr bool contains_calling_policy = (std::is_same_v || ... ); + static_assert( (!contains_calling_policy) || AllowCallingPolicy, "calling_policy can only be set for raw function pointers!"); + + // chat that the finalize_policy is only set if explicitly allowed + constexpr bool contains_finalize_policy = (std::is_same_v || ... ); + static_assert( (!contains_finalize_policy) || AllowFinalizePolicy, "finalize_policy can only be set for constructors!"); + + ExtraFunctionData result; + + (parse_attributes_helper(result, std::move(extra)), ...); + + return result; + } + + /// count occurences of specific type in parameter pack + template + constexpr int count_attributes() + { + return (0 + ... + int(std::is_same_v)); + } + + static_assert(count_attributes() == 1); + static_assert(count_attributes() == 4); + + /// check number of arguments matches annotated arguments if annotations for keyword arguments are present + template + constexpr bool check_extra_argument_count(int n_arg) + { + // with keyword arguments, the number of annotated arguments must match the number of actual arguments + constexpr auto n_extra_arg = count_attributes(); + constexpr auto n_extra_kwarg = count_attributes(); + return n_extra_kwarg == 0 || n_arg == n_extra_arg + n_extra_kwarg; + } + + /// simple helper for checking if a template argument has a call operator (e.g. is a lambda) + template + struct has_call_operator : std::false_type {}; + + template + struct has_call_operator> : std::true_type {}; + + static_assert(!has_call_operator::value); + static_assert(has_call_operator>::value); +} + +} + +#endif diff --git a/include/jlcxx/module.hpp b/include/jlcxx/module.hpp index 7af40f2..ec84f61 100644 --- a/include/jlcxx/module.hpp +++ b/include/jlcxx/module.hpp @@ -11,6 +11,7 @@ #include #include "array.hpp" +#include "attr.hpp" #include "type_conversion.hpp" namespace jlcxx @@ -179,6 +180,32 @@ class JLCXX_API FunctionWrapperBase return m_doc; } + void set_extra_argument_data(std::vector&& posArgs, std::vector&& kwArgs) + { + m_number_of_keyword_args = kwArgs.size(); + + // gather all argument names + m_argument_names.clear(); + for(auto& a: posArgs) + m_argument_names.push_back(jl_cstr_to_string(a.name)); + for(auto& a: kwArgs) + m_argument_names.push_back(jl_cstr_to_string(a.name)); + // ensure the Julia GC doesn't throw away our strings: + for(auto& s: m_argument_names) + protect_from_gc(s); + + // gather all default values + m_argument_default_values.clear(); + for(auto& a: posArgs) + m_argument_default_values.push_back(a.defaultValue); + for(auto& a: kwArgs) + m_argument_default_values.push_back(a.defaultValue); + } + + const std::vector& argument_names() const {return m_argument_names;} + int number_of_keyword_arguments() const {return m_number_of_keyword_args;} + const std::vector& argument_default_values() const {return m_argument_default_values;} + inline void set_override_module(jl_module_t* mod) { m_override_module = (jl_value_t*)mod; } inline jl_value_t* override_module() const { return m_override_module; } @@ -191,11 +218,15 @@ class JLCXX_API FunctionWrapperBase private: jl_value_t* m_name = nullptr; jl_value_t* m_doc = nullptr; + std::vector m_argument_names; + int m_number_of_keyword_args = 0; + std::vector m_argument_default_values; Module* m_module; std::pair m_return_type = std::make_pair(nullptr,nullptr); // The module in which the function is overridden, e.g. jl_base_module when trying to override Base.getindex. jl_value_t* m_override_module = nullptr; + }; /// Implementation of function storage, case of std::function @@ -532,63 +563,73 @@ class JLCXX_API Module } /// Define a new function - template - FunctionWrapperBase& method(const std::string& name, std::function f, const std::string& doc = std::string()) + template + FunctionWrapperBase& method(const std::string& name, std::function f, Extra... extra) { - auto* new_wrapper = new FunctionWrapper(this, f); - new_wrapper->set_name((jl_value_t*)jl_symbol(name.c_str())); - new_wrapper->set_doc(jl_cstr_to_string(doc.c_str())); - append_function(new_wrapper); - return *new_wrapper; + static_assert(detail::check_extra_argument_count(sizeof...(Args)), "Wrong number of annotated arguments (jlcxx::arg and jlcxx::kwarg arguments)!"); + + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + return method_helper(name, f, extraData); } /// Define a new function. Overload for pointers - template - FunctionWrapperBase& method(const std::string& name, R(*f)(Args...), const std::string& doc = std::string(), const bool force_convert = false) + template + FunctionWrapperBase& method(const std::string& name, R(*f)(Args...), Extra... extra) { - const bool need_convert = force_convert || detail::NeedConvertHelper()(); + static_assert(detail::check_extra_argument_count(sizeof...(Args)), "Wrong number of annotated arguments (jlcxx::arg and jlcxx::kwarg arguments)!"); + + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + const bool need_convert = bool(extraData.force_convert) || detail::NeedConvertHelper()(); // Conversion is automatic when using the std::function calling method, so if we need conversion we use that if(need_convert) { - return method(name, std::function(f), doc); + return method_helper(name, std::function(f), std::move(extraData)); } // No conversion needed -> call can be through a naked function pointer auto* new_wrapper = new FunctionPtrWrapper(this, f); new_wrapper->set_name((jl_value_t*)jl_symbol(name.c_str())); - new_wrapper->set_doc(jl_cstr_to_string(doc.c_str())); + new_wrapper->set_doc(jl_cstr_to_string(extraData.doc.c_str())); + new_wrapper->set_extra_argument_data(std::move(extraData.positionalArguments), std::move(extraData.keywordArguments)); append_function(new_wrapper); return *new_wrapper; } /// Define a new function. Overload for lambda - template - FunctionWrapperBase& method(const std::string& name, LambdaT&& lambda, const std::string& doc = std::string(), typename std::enable_if::value, bool>::type = true) + template::value && !std::is_member_function_pointer::value, bool> = true> + FunctionWrapperBase& method(const std::string& name, LambdaT&& lambda, Extra... extra) { - return add_lambda(name, std::forward(lambda), &LambdaT::operator(), doc); + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + return lambda_helper(name, std::forward(lambda), &LambdaT::operator(), std::move(extraData)); } /// Add a constructor with the given argument types for the given datatype (used to get the name) - template - void constructor(jl_datatype_t* dt, bool finalize=true, const std::string& doc = std::string()) + template + void constructor(jl_datatype_t* dt, Extra... extra) { - FunctionWrapperBase &new_wrapper = finalize ? method("dummy", [](ArgsT... args) { return create(args...); }) : method("dummy", [](ArgsT... args) { return create(args...); }); + static_assert(detail::check_extra_argument_count(sizeof...(ArgsT)), "Wrong number of annotated arguments (jlcxx::arg and jlcxx::kwarg arguments)!"); + + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + FunctionWrapperBase &new_wrapper = bool(extraData.finalize) ? add_lambda("dummy", [](ArgsT... args) { return create(args...); }, std::move(extraData)) : add_lambda("dummy", [](ArgsT... args) { return create(args...); }, std::move(extraData)); new_wrapper.set_name(detail::make_fname("ConstructorFname", dt)); - new_wrapper.set_doc(jl_cstr_to_string(doc.c_str())); + new_wrapper.set_doc(jl_cstr_to_string(extraData.doc.c_str())); + new_wrapper.set_extra_argument_data(std::move(extraData.positionalArguments), std::move(extraData.keywordArguments)); } - template - void constructor(jl_datatype_t* dt, LambdaT&& lambda, R(LambdaT::*)(ArgsT...) const, bool finalize, const std::string& doc = std::string()) + template + void constructor(jl_datatype_t* dt, LambdaT&& lambda, R(LambdaT::*)(ArgsT...) const, Extra... extra) { static_assert(std::is_same::value, "Constructor lambda function must return a pointer to the constructed object, of the correct type"); - FunctionWrapperBase &new_wrapper = method("dummy", [=](ArgsT... args) + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + FunctionWrapperBase &new_wrapper = add_lambda("dummy", [=](ArgsT... args) { jl_datatype_t* concrete_dt = julia_type(); assert(jl_is_mutable_datatype(concrete_dt)); T* cpp_obj = lambda(std::forward(args)...); - return boxed_cpp_pointer(cpp_obj, concrete_dt, finalize); - }, doc); + return boxed_cpp_pointer(cpp_obj, concrete_dt, bool(extraData.finalize)); + }, std::move(extraData)); new_wrapper.set_name(detail::make_fname("ConstructorFname", dt)); } @@ -711,10 +752,27 @@ class JLCXX_API Module template TypeWrapper add_type_internal(const std::string& name, JLSuperT* super); + template + FunctionWrapperBase& add_lambda(const std::string& name, LambdaT&& lambda, detail::ExtraFunctionData&& extraData) + { + return lambda_helper(name, std::forward(lambda), &LambdaT::operator(), std::move(extraData)); + } + template - FunctionWrapperBase& add_lambda(const std::string& name, LambdaT&& lambda, R(LambdaT::*)(ArgsT...) const, const std::string& doc = std::string()) + FunctionWrapperBase& lambda_helper(const std::string& name, LambdaT&& lambda, R(LambdaT::*)(ArgsT...) const, detail::ExtraFunctionData&& extraData) + { + return method_helper(name, std::function(std::forward(lambda)), std::move(extraData)); + } + + template + FunctionWrapperBase& method_helper(const std::string& name, std::function f, detail::ExtraFunctionData&& extraData) { - return method(name, std::function(std::forward(lambda)), doc); + auto* new_wrapper = new FunctionWrapper(this, f); + new_wrapper->set_name((jl_value_t*)jl_symbol(name.c_str())); + new_wrapper->set_doc(jl_cstr_to_string(extraData.doc.c_str())); + new_wrapper->set_extra_argument_data(std::move(extraData.positionalArguments), std::move(extraData.keywordArguments)); + append_function(new_wrapper); + return *new_wrapper; } void set_constant(const std::string& name, jl_value_t* boxed_const); @@ -999,72 +1057,77 @@ class TypeWrapper } /// Add a constructor with the given argument types - template - TypeWrapper& constructor(bool finalize=true) + template + TypeWrapper& constructor(Extra... extra) { // Only add the default constructor if it wasn't added automatically if constexpr (!(DefaultConstructible::value && sizeof...(ArgsT) == 0)) { - m_module.constructor(m_dt, finalize); + m_module.constructor(m_dt, extra...); } return *this; } /// Define a "constructor" using a lambda - template - TypeWrapper& constructor(LambdaT&& lambda, bool finalize = true) + template::value, bool> = true> + TypeWrapper& constructor(LambdaT&& lambda, Extra... extra) { - m_module.constructor(m_dt, std::forward(lambda), &LambdaT::operator(), finalize); + m_module.constructor(m_dt, std::forward(lambda), &LambdaT::operator(), extra...); return *this; } /// Define a member function - template - TypeWrapper& method(const std::string& name, R(CT::*f)(ArgsT...), const std::string& doc = std::string()) + template + TypeWrapper& method(const std::string& name, R(CT::*f)(ArgsT...), Extra... extra) { - m_module.method(name, [f](T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, doc ); - m_module.method(name, [f](T* obj, ArgsT... args) -> R { return ((*obj).*f)(args...); }, doc ); + m_module.method(name, [f](T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, extra... ); + m_module.method(name, [f](T* obj, ArgsT... args) -> R { return ((*obj).*f)(args...); }, extra... ); return *this; } /// Define a member function, const version - template - TypeWrapper& method(const std::string& name, R(CT::*f)(ArgsT...) const, const std::string& doc = std::string()) + template + TypeWrapper& method(const std::string& name, R(CT::*f)(ArgsT...) const, Extra... extra) { - m_module.method(name, [f](const T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, doc ); - m_module.method(name, [f](const T* obj, ArgsT... args) -> R { return ((*obj).*f)(args...); }, doc ); + m_module.method(name, [f](const T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, extra... ); + m_module.method(name, [f](const T* obj, ArgsT... args) -> R { return ((*obj).*f)(args...); }, extra... ); return *this; } /// Define a "member" function using a lambda - template - TypeWrapper& method(const std::string& name, LambdaT&& lambda, const std::string& doc = std::string(), typename std::enable_if::value, bool>::type = true) + template::value && !std::is_member_function_pointer::value, bool> = true> + TypeWrapper& method(const std::string& name, LambdaT&& lambda, Extra... extra) { - m_module.method(name, std::forward(lambda), doc); + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + m_module.lambda_helper(name, std::forward(lambda), &LambdaT::operator(), std::move(extraData)); return *this; } /// Call operator overload. For concrete type box to work around https://github.com/JuliaLang/julia/issues/14919 - template - TypeWrapper& method(R(CT::*f)(ArgsT...), const std::string& doc = std::string()) + template + TypeWrapper& method(R(CT::*f)(ArgsT...), Extra... extra) { - m_module.method("operator()", [f](T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, doc ) + m_module.method("operator()", [f](T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, extra... ) .set_name(detail::make_fname("CallOpOverload", m_box_dt)); return *this; } - template - TypeWrapper& method(R(CT::*f)(ArgsT...) const, const std::string& doc = std::string()) + template + TypeWrapper& method(R(CT::*f)(ArgsT...) const, Extra... extra) { - m_module.method("operator()", [f](const T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, doc ) + m_module.method("operator()", [f](const T& obj, ArgsT... args) -> R { return (obj.*f)(args...); }, extra... ) .set_name(detail::make_fname("CallOpOverload", m_box_dt)); return *this; } /// Overload operator() using a lambda - template - TypeWrapper& method(LambdaT&& lambda, const std::string& doc = std::string()) + template::value, bool> = true> + TypeWrapper& method(LambdaT&& lambda, Extra... extra) { - m_module.method("operator()", std::forward(lambda), doc) + detail::ExtraFunctionData extraData = detail::parse_attributes(extra...); + m_module.lambda_helper("operator()", std::forward(lambda), &LambdaT::operator(), std::move(extraData)) .set_name(detail::make_fname("CallOpOverload", m_box_dt)); return *this; } diff --git a/src/c_interface.cpp b/src/c_interface.cpp index 62bc5d9..e10f479 100644 --- a/src/c_interface.cpp +++ b/src/c_interface.cpp @@ -120,6 +120,14 @@ void fill_types_vec(Array& types_array, const std::vector& types_array, const std::vector& types_vec) +{ + for(const auto& t : types_vec) + { + types_array.push_back(t); + } +} + /// Get the functions defined in the modules. Any classes used by these functions must be defined on the Julia side first JLCXX_API jl_array_t* get_module_functions(jl_module_t* jlmod) { @@ -132,13 +140,21 @@ JLCXX_API jl_array_t* get_module_functions(jl_module_t* jlmod) Array arg_types_array; jl_value_t* boxed_f = nullptr; jl_value_t* boxed_thunk = nullptr; - JL_GC_PUSH3(arg_types_array.gc_pointer(), &boxed_f, &boxed_thunk); + Array arg_names_array; + Array arg_default_values_array; + jl_value_t* boxed_n_kwargs; + JL_GC_PUSH6(arg_types_array.gc_pointer(), &boxed_f, &boxed_thunk, arg_names_array.gc_pointer(), arg_default_values_array.gc_pointer(), &boxed_n_kwargs); fill_types_vec(arg_types_array, f.argument_types()); boxed_f = jlcxx::box(f.pointer()); boxed_thunk = jlcxx::box(f.thunk()); + fill_values_vec(arg_names_array, f.argument_names()); + fill_values_vec(arg_default_values_array, f.argument_default_values()); + + boxed_n_kwargs = jlcxx::box(f.number_of_keyword_arguments()); + auto returntypes = f.return_type(); jl_datatype_t* ccall_return_type = returntypes.first; @@ -157,7 +173,10 @@ JLCXX_API jl_array_t* get_module_functions(jl_module_t* jlmod) boxed_f, boxed_thunk, f.override_module(), - f.doc() + f.doc(), + arg_names_array.wrapped(), + arg_default_values_array.wrapped(), + boxed_n_kwargs )); JL_GC_POP();