diff --git a/.src/array_iterator.inl b/.src/array_iterator.inl index 50701fd..9a002e5 100644 --- a/.src/array_iterator.inl +++ b/.src/array_iterator.inl @@ -59,17 +59,15 @@ namespace jluna } template - template - T Array::ConstIterator::operator*() const + auto Array::ConstIterator::operator*() { - static jl_function_t* getindex = jl_get_function(jl_base_module, "getindex"); - return unbox(jluna::safe_call(getindex, _owner->operator const jl_value_t *(), box(_index + 1))); + return Iterator(_index, const_cast*>(_owner)); } template - auto Array::ConstIterator::operator*() + auto Array::ConstIterator::operator*() const { - return Iterator(_index, const_cast*>(_owner)); + return *this; } template @@ -88,10 +86,11 @@ namespace jluna } template - template, bool>> + template, bool>> Array::ConstIterator::operator T() const { - return operator*(); + static jl_function_t* getindex = jl_get_function(jl_base_module, "getindex"); + return unbox(jluna::safe_call(getindex, _owner->operator const jl_value_t *(), box(_index + 1))); } template diff --git a/.src/c_adapter.cpp b/.src/c_adapter.cpp index 6861f6a..cad0106 100644 --- a/.src/c_adapter.cpp +++ b/.src/c_adapter.cpp @@ -1,6 +1,6 @@ #ifdef __cplusplus -#include +#include #include diff --git a/.src/c_adapter.hpp b/.src/c_adapter.hpp index 65b1138..261f933 100644 --- a/.src/c_adapter.hpp +++ b/.src/c_adapter.hpp @@ -10,7 +10,7 @@ #ifdef __cplusplus #include -#include +#include #include #include diff --git a/.src/include_julia.inl.in b/.src/include_julia.inl.in index 65d5fa2..80ce1d8 100644 --- a/.src/include_julia.inl.in +++ b/.src/include_julia.inl.in @@ -5,12 +5,40 @@ #pragma once -#cmakedefine RESOURCE_PATH "@RESOURCE_PATH@" +#include namespace jluna::detail { - ///@brief allow julia to load jluna by using C++ #import statement - static inline const char* include = R"( - include("@RESOURCE_PATH@/include/jluna.jl") + static inline const char* c_adapter_path = "@C_ADAPTER_NAME@"; + static inline std::string c_adapter_path_override = ""; + + // source split like this because of: https://docs.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2026?view=msvc-160 + + static inline const char* module_start = "module jluna"; + + static inline const char* include_01 = R"( + @01_COMMON@ + )"; + + static inline const char* include_02 = R"( + @02_COMMON@ + )"; + + static inline const char* include_03 = R"( + @03_EXCEPTION_HANDLER@ + )"; + + static inline const char* include_04 = R"( + @04_MEMORY_HANDLER@ + )"; + + static inline const char* include_05 = R"( + @05_CPPCALL@ + )"; + + static inline const char* module_end = "end"; + + static inline const char* include_06 = R"( + @06_PUBLIC@ )"; } \ No newline at end of file diff --git a/.src/state.cpp b/.src/state.cpp index 080e804..d2148c7 100644 --- a/.src/state.cpp +++ b/.src/state.cpp @@ -3,7 +3,7 @@ // Created on 31.01.22 by clem (mail@clemens-cords.com) // -#include +#include #include #include @@ -13,10 +13,9 @@ #include #include #include -#include <.src/include_julia.inl> #include #include - +#include <.src/include_julia.inl> namespace jluna::detail { @@ -30,6 +29,11 @@ namespace jluna::detail namespace jluna::State { + void set_c_adapter_path(const std::string& path) + { + jluna::detail::c_adapter_path_override = path; + } + void initialize() { initialize(""); @@ -42,9 +46,23 @@ namespace jluna::State else jl_init_with_image(path.c_str(), NULL); - jl_eval_string(jluna::detail::include); - forward_last_exception(); + { // execute jluna julia code in pieces + using namespace jluna::detail; + std::stringstream str; + str << module_start; + str << include_01; + str << include_02; + str << include_03; + str << include_04; + str << include_05; + str << module_end; + str << include_06; + + jl_eval_string(str.str().c_str()); + } + + forward_last_exception(); jl_eval_string(R"( begin @@ -57,7 +75,12 @@ namespace jluna::State )"); forward_last_exception(); - jl_eval_string(("jluna._cppcall.eval(:(_library_name = \"" + std::string(RESOURCE_PATH) + "/libjluna_c_adapter.so\"))").c_str()); + std::stringstream str; + str << "jluna._cppcall.eval(:(_library_name = \""; + str << (jluna::detail::c_adapter_path_override.empty() ? jluna::detail::c_adapter_path : jluna::detail::c_adapter_path_override); + str << "\"))"; + + jl_eval_string(str.str().c_str()); forward_last_exception(); jl_eval_string(R"( @@ -214,7 +237,7 @@ namespace jluna::State::detail Any * get_reference(size_t key) { static Function* get_reference = jl_find_function("jluna.memory_handler", "get_reference"); - return jluna::safe_call(get_reference, jl_box_uint64(reinterpret_cast(key))); + return jluna::safe_call(get_reference, jl_box_uint64(static_cast(key))); } void free_reference(size_t key) @@ -226,7 +249,7 @@ namespace jluna::State::detail static Function* free_reference = jl_find_function("jluna.memory_handler", "free_reference"); jl_gc_pause; - jluna::safe_call(free_reference, jl_box_uint64(reinterpret_cast(key))); + jluna::safe_call(free_reference, jl_box_uint64(static_cast(key))); jl_gc_unpause; } diff --git a/.test/main.cpp b/.test/main.cpp index 754e5c3..2d28c99 100644 --- a/.test/main.cpp +++ b/.test/main.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include <.test/test.hpp> diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a5621d..e13d948 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,86 +1,94 @@ -cmake_minimum_required(VERSION 3.16) -project(jluna) +cmake_minimum_required(VERSION 3.12) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_BUILD_TYPE Debug) +#[=======================================================================[.rst: -if (WIN32) - message(WARNING "Windows support is experimental, some features my not work correctly. It maybe be necessary to fork jluna and modify jluna/CMakeLists.txt") -endif() +Build jluna +----------- -### COMPILER SUPPORT ### +This cmake attempts to automatically detect the julia image. +If this is not possible, manually specify the path to the julia binary using: -if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "12.0.0") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-volatile") -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 -Wno-volatile") -elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "19.20") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") -else() - message(FATAL_ERROR "Currently, the only supported compilers are G++10 (or higher), Clang-12 (or higher) and MSVC 19.30 (experimental)") -endif() + ``-DJULIA_BINDIR=/path/to/.../julia/bin`` -### JULIA ### - -if (WIN32) - if (NOT DEFINED ENV{JULIA_PATH}) - message(FATAL_ERROR "Cannot determine location of julia image. Before running cmake, please manually set the environment variable JULIA_PATH using:\n\tset JULIA_PATH=\"C:/path/to/your/.../julia-1.7.2\"\nIf you are unsure of the location of your julia image, you can access the path from within the julia REPL using\n println(joinpath(Sys.BINDIR, \"..\"))") - endif() -else() - # access julia through julia environment variable - if (NOT DEFINED ENV{JULIA_PATH}) - execute_process( - COMMAND julia -e "println(joinpath(Sys.BINDIR, \"..\"))" - OUTPUT_VARIABLE JULIA_PATH_LOCAL) - - if ("${JULIA_PATH}" STREQUAL "") - message(FATAL_ERROR "Cannot determine location of julia image. Before running cmake, please manually set the environment variable JULIA_PATH using\n export JULIA_PATH=/path/to/your/.../julia\nIf you are unsure of the location of your julia image, you can access the path from within the julia REPL using\n println(joinpath(Sys.BINDIR, \"..\")) - For more information, visit https://github.com/Clemapfel/jluna/blob/master/README.md#troubleshooting") - endif() - - set(ENV{JULIA_PATH} ${JULIA_PATH_LOCAL}) - endif() -endif() +If you are unsure of the location of this folder, you can access +it from within the REPL using: -if (WIN32) - set(JULIA_LIB "$ENV{JULIA_PATH}/lib/libjulia.dll.a") -else() - set(JULIA_LIB "$ENV{JULIA_PATH}/lib/libjulia.so") -endif() + ``println(Sys.BINDIR)`` -set(JULIA_INCLUDE "$ENV{JULIA_PATH}/include/julia") -set(JULIA_FOUND TRUE) - -# verify shared library -if (NOT EXISTS ${JULIA_LIB}) - if (WIN32) - message(WARNING "Cannot find shared library libjulia.dll.a in $ENV{JULIA_PATH}/lib/") - else() - message(WARNING "Cannot find shared library libjulia.so in $ENV{JULIA_PATH}/lib/") - endif() - set(JULIA_FOUND FALSE) -endif() +Options +^^^^^^^ +``jluna_DEVELOPER_MODE`` + enable building test and benchmark executables. Off by default +``BUILD_TESTING`` + build jluna_test, as CTest. On by default +``BUILD_BENCHMARK`` + build jluna_benchmark. On by default -# verify header -if (NOT EXISTS ${JULIA_INCLUDE}/julia.h) - message(WARNING "Cannot find library header julia.h in $ENV{JULIA_PATH}/include/julia/") - set(JULIA_FOUND FALSE) -endif() +#]=======================================================================] -if (${JULIA_FOUND}) - message("[LOG] Successfully detected julia image at $ENV{JULIA_PATH}") -else() - message(FATAL_ERROR "Failed to detect julia image. Make sure JULIA_PATH is set correctly and that the julia image is uncompressed and not corrupted.\nFor more information, visit https://github.com/Clemapfel/jluna/blob/master/README.md#troubleshooting") +project(jluna VERSION 0.8.4 LANGUAGES CXX) + +include(cmake/project-is-top-level.cmake) +if(PROJECT_IS_TOP_LEVEL) + option(jluna_DEVELOPER_MODE "Enable developer mode" OFF) endif() -include_directories(${JULIA_INCLUDE}) +### Find Julia ### + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/find") +find_package(Julia 1.7.0 REQUIRED) + +### Configure Files ### + +# TODO: don't hardcode these +set(C_ADAPTER_PATH "${CMAKE_INSTALL_PREFIX}") +set(C_ADAPTER_NAME "${C_ADAPTER_PATH}/${CMAKE_SHARED_LIBRARY_PREFIX}jluna_c_adapter${CMAKE_SHARED_LIBRARY_SUFFIX}") + +file(READ + include/julia/01_common.jl + 01_COMMON +) + +file(READ + include/julia/02_common.jl + 02_COMMON +) + +file(READ + include/julia/03_exception_handler.jl + 03_EXCEPTION_HANDLER +) + +file(READ + include/julia/04_memory_handler.jl + 04_MEMORY_HANDLER +) + +file(READ + include/julia/05_cppcall.jl + 05_CPPCALL +) -### JLUNA ### +file(READ + include/julia/06_public.jl + 06_PUBLIC +) -include_directories(${CMAKE_SOURCE_DIR}) +configure_file("${CMAKE_SOURCE_DIR}/.src/include_julia.inl.in" "${CMAKE_SOURCE_DIR}/.src/include_julia.inl" @ONLY) -set(RESOURCE_PATH ${CMAKE_SOURCE_DIR}) -configure_file(${CMAKE_SOURCE_DIR}/.src/include_julia.inl.in ${CMAKE_SOURCE_DIR}/.src/include_julia.inl @ONLY) +### Declare C adapter ### + +add_library( + jluna_c_adapter SHARED + .src/c_adapter.hpp + .src/c_adapter.cpp +) + +target_compile_features(jluna_c_adapter PUBLIC cxx_std_20) +target_include_directories(jluna_c_adapter PUBLIC "$") +target_link_libraries(jluna_c_adapter PUBLIC "$") + +### Declare Library ### add_library(jluna SHARED jluna.hpp @@ -143,71 +151,59 @@ add_library(jluna SHARED include/gc_sentinel.hpp ) -set_target_properties(jluna PROPERTIES - LINKER_LANGUAGE C - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR} -) -target_compile_features(jluna PUBLIC cxx_std_20) - -### C ADAPTER ## - -add_library(jluna_c_adapter SHARED - .src/c_adapter.hpp - .src/c_adapter.cpp -) - -target_include_directories(jluna_c_adapter - PUBLIC $ - PUBLIC $ -) - -target_link_libraries(jluna_c_adapter PUBLIC $) - -set_target_properties(jluna_c_adapter PROPERTIES - LINKER_LANGUAGE C - LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR} -) - target_link_libraries(jluna PUBLIC jluna_c_adapter) +target_include_directories(jluna PRIVATE "$") -### EXECUTABLES ### +### HACK: export all symbols on Windows ### -add_executable(JLUNA_TEST - .test/main.cpp - .test/test.hpp +set_target_properties(jluna jluna_c_adapter PROPERTIES + WINDOWS_EXPORT_ALL_SYMBOLS YES # TODO + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION}" + LIBRARY_OUTPUT_DIRECTORY "${C_ADAPTER_PATH}" ) -target_link_libraries(JLUNA_TEST jluna ${JULIA_LIB}) +# Fix this using the GenerateExportHeader CMake module and visibility properties. -add_executable(JLUNA_BENCHMARK - .benchmark/main.cpp - .benchmark/benchmark.hpp - .benchmark/benchmark_aux.hpp -) -target_link_libraries(JLUNA_BENCHMARK jluna ${JULIA_LIB}) - -### INSTALL ### +### Install rules ### -file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/jluna-config.cmake [[ -if (NOT ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "12.0.0") OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION STRGREATER_EQUAL "10.0.0"))) - message(FATAL_ERROR "Currently, the only supported compilers are G++10, G++11 (recommended) and Clang-12") +if(NOT CMAKE_SKIP_INSTALL_RULES) + include(cmake/install-rules.cmake) endif() -include("${CMAKE_CURRENT_LIST_DIR}/jluna-targets.cmake") -]]) -install(FILES - ${CMAKE_CURRENT_BINARY_DIR}/jluna-config.cmake - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jluna -) +### Developer mode ### -install(TARGETS jluna jluna_c_adapter EXPORT jluna_targets) - -install(EXPORT jluna_targets - FILE jluna-targets.cmake - NAMESPACE jluna:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/jluna -) +if(NOT jluna_DEVELOPER_MODE) + return() +elseif(NOT PROJECT_IS_TOP_LEVEL) + message( + AUTHOR_WARNING + "Developer mode is intended for developers of jluna" + ) +endif() -install(FILES jluna.hpp ${headers} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/jluna) -install(DIRECTORY include .src DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/jluna) +include(CTest) +### Declare Test ### + +option(BUILD_TESTING "Enable jluna_test" ON) +if (BUILD_TESTING) + add_executable( + jluna_test + .test/main.cpp + .test/test.hpp + ) + target_link_libraries(jluna_test PRIVATE jluna) + add_test(NAME jluna_test COMMAND jluna_test) +endif() -target_include_directories(jluna INTERFACE $) \ No newline at end of file +### Declare Benchmark ### + +option(BUILD_BENCHMARK "Enable jluna_benchmark" ON) +if (BUILD_BENCHMARK AND NOT WIN32) + add_executable( + jluna_benchmark + .benchmark/main.cpp + .benchmark/benchmark.hpp + .benchmark/benchmark_aux.hpp + ) + target_link_libraries(jluna_benchmark PRIVATE jluna) +endif() diff --git a/README.md b/README.md index a9fd258..bfcf4d4 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,7 @@ Heavily inspired in design and syntax by (but in no way affiliated with) the exc 5. [Dependencies](#dependencies)
5.1 [julia 1.7.0+](#dependencies)
5.2 [Supported Compilers: gcc10, gcc11, clang12](#dependencies)
- 5.3 [cmake 3.16+](#dependencies)
- 5.4 [unix-based OS](#dependencies)
+ 5.3 [cmake 3.12+](#dependencies)
6. [License](#license) 7. [Authors](#credits) @@ -147,6 +146,7 @@ NonJuliaType(["new"]) Some of the many advantages `jluna` has over the C-API include: + expressive, generic syntax ++ automatically detects and links julia + call C++ functions from julia using any julia-type + assigning C++-side proxies also mutates the corresponding variable julia-side + any C++ type can be moved between Julia and C++. Any julia-type can be wrapped @@ -184,9 +184,8 @@ Advanced users are encouraged to check the headers (available in `jluna/include/ For `jluna` you'll need: + [**Julia 1.7.0**](https://julialang.org/downloads/#current_stable_release) (or higher) -+ [**cmake 3.16**](https://cmake.org/download/) (or higher) ++ [**cmake 3.12**](https://cmake.org/download/) (or higher) + C++ Compiler (see below) -+ unix-based* 64-bit operating system Currently [**g++10**](https://askubuntu.com/questions/1192955/how-to-install-g-10-on-ubuntu-18-04), [**g++11**](https://lindevs.com/install-g-on-ubuntu/) and [**clang++-12**](https://linux-packages.com/ubuntu-focal-fossa/package/clang-12) are fully supported. g++-11 is the primary compiler used for development of `jluna` and is thus recommended. MSVC is untested but may work. @@ -196,52 +195,59 @@ Currently [**g++10**](https://askubuntu.com/questions/1192955/how-to-install-g-1 ## [Installation & Troubleshooting](./docs/installation.md) -A step-by-step tutorial on how to create, compile, and link a new C++ Project with `jluna` can be found [here](./docs/installation.md). It is recommended that you follow this guide closely, instead of trying to resolve issues on your own. -### For Advanced Users Only +> A step-by-step guide intended for users unfamiliar with cmake is available [here](./docs/installation.md) -Users familiar with C++ and cmake can go through the following steps (on unix-based operating systems): - -Install: - -+ `g++-11` (or `clang++-12`) -+ `julia 1.7+` -+ `cmake 3.16+` - -Then execute (in the same directory as your `CMakeLists.txt`): +Execute, in any public directory ```bash -git clone https://github.com/Clemapfel/jluna.git - -export JULIA_PATH=$(julia -e "println(joinpath(Sys.BINDIR, \"..\"))") - +git clone https://github.com/Clemapfel/jluna +cd jluna mkdir build cd build -cmake -D CMAKE_CXX_COMPILER=g++-11 .. # or clang++-12 -make +cmake .. -DCMAKE_CXX_COMPILER= +make install +``` -./JLUNA_TEST +Where ++ `` is one of `g++-10`, `g++-11`, `clang++-12` -cd .. -rm -r build +Afterwards, you can make `jluna` available to your library using + +```cmake +# inside your own CMakeLists.txt +find_library(jluna REQUIRED) +target_link_libraries( PRIVATE + "${jluna}" + "${}") ``` +Where ++ `` is the Julia Library (usually available in `"${JULIA_BINDIR}/../lib"`) ++ `` is the name of your library or executable + +If errors appear at any point, head to [troubleshooting](./docs/installation.md#troubleshooting). -Where `JULIA_PATH` needs to be set at the time of compilation. +--- -Link against `jluna/libjluna.so`, `jluna/libjluna_c_adapter.so` and `$ENV{JULIA_PATH}/lib/libjulia.so`. +### Creating a Project from Scratch -Add `"${CMAKE_SOURCE_DIR}/jluna"` and `"$ENV{JULIA_PATH}/include/julia"` to your include directories. +`jluna` offers a one-line wizard for installing it and creating a new project. This option is only recommended for novice users, more experienced users should create the project themself and link it as detailed above. -Then you can make `jluna` available to your library using: +> this feature is only available on unix systems + +Download `init.sh` [here](https://raw.githubusercontent.com/Clemapfel/jluna/cmake_rework/install/init.sh). Then, execute (in the same folder you downloaded `init.sh` to): ```cpp -#include -#include +/bin/bash init.sh ``` +Where ++ `` is the name of your desired project folder, for example `MyProject` ++ `` is the root path to your new project folder, for example `/home/user/Desktop` ++ `` is one of `g++-10`, `g++-11`, `clang++-12` -If errors appear at any point, head to the [step-by-step guide](./docs/installation.md). +The bash script will create a folder in `/`, install jluna in that folder and setup a working hello_world executable for you. ---- +If errors appear at any point, head to [troubleshooting](./docs/installation.md#troubleshooting). ## License diff --git a/cmake/find/FindJulia.cmake b/cmake/find/FindJulia.cmake new file mode 100644 index 0000000..daa11c1 --- /dev/null +++ b/cmake/find/FindJulia.cmake @@ -0,0 +1,148 @@ +#[=======================================================================[.rst: + +FindJulia +----------- + +This module is intended to detect julia as a package. + +Imported Targets +^^^^^^^^^^^^^^^^ + +``Julia::Julia`` + Julia Package, if found + +Result Variables +^^^^^^^^^^^^^^^^ + +``JULIA_LIBRARY`` + julia shared library +``JULIA_EXECUTABLE`` + julia REPL executable +``JULIA_BINDIR`` + directory to julia binary +``JULIA_INCLUDE_DIR`` + directory that contains julia.h + +Usage +^^^^^ +Use: + ``list(APPEND CMAKE_MODULE_PATH "path/to/this/file")`` + ``find_package(Julia 1.7.0 REQUIRED)`` + +to make the julia target available to your target through + ``"$"`` + +#]=======================================================================] + +macro(julia_bail_if_false message var) + if(NOT ${var}) + set(Julia_FOUND 0) + set(Julia_NOT_FOUND_MESSAGE "${message}") + return() + endif() +endmacro() + +# detect julia executable +find_program(JULIA_EXECUTABLE julia PATHS ENV JULIA_BINDIR) +julia_bail_if_false("Unable to detect the julia executable. Make sure JULIA_BINDIR is set correctly." JULIA_EXECUTABLE) + +# detect julia binary dir +if(NOT DEFINED JULIA_BINDIR) + # The executable could be a chocolatey shim, so run some Julia code to report + # the path of the BINDIR + execute_process( + COMMAND "${JULIA_EXECUTABLE}" -e "print(Sys.BINDIR)" + OUTPUT_VARIABLE JULIA_BINDIR_LOCAL + ) + file(TO_CMAKE_PATH "${JULIA_BINDIR_LOCAL}" JULIA_BINDIR_LOCAL) + set(JULIA_BINDIR "${JULIA_BINDIR_LOCAL}" CACHE PATH "") +endif() +get_filename_component(JULIA_PATH_PREFIX "${JULIA_BINDIR}" DIRECTORY) + +if(WIN32) + set(julia_old_CMAKE_FIND_LIBRARY_SUFFIXES "") + set(julia_old_CMAKE_FIND_LIBRARY_PREFIXES "") + if(CMAKE_FIND_LIBRARY_SUFFIXES) + set(julia_old_CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_FIND_LIBRARY_SUFFIXES}") + list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES .dll.a) + else() + set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib;.dll.a") + endif() + if(CMAKE_FIND_LIBRARY_PREFIXES) + set(julia_old_CMAKE_FIND_LIBRARY_PREFIXES "${CMAKE_FIND_LIBRARY_PREFIXES}") + list(APPEND CMAKE_FIND_LIBRARY_PREFIXES lib) + else() + set(CMAKE_FIND_LIBRARY_PREFIXES ";lib") + endif() +endif() + +# detect julia library +find_library(JULIA_LIBRARY julia HINTS "${JULIA_PATH_PREFIX}/lib") + +if(WIN32) + set(CMAKE_FIND_LIBRARY_SUFFIXES "${julia_old_CMAKE_FIND_LIBRARY_SUFFIXES}") + set(CMAKE_FIND_LIBRARY_PREFIXES "${julia_old_CMAKE_FIND_LIBRARY_PREFIXES}") +endif() + +julia_bail_if_false("Unable to find the julia shared library. Make sure JULIA_BINDIR is set correctly and that the julia image is uncompressed" JULIA_LIBRARY) + +# detect julia include dir +find_path( + JULIA_INCLUDE_DIR julia.h + HINTS "${JULIA_PATH_PREFIX}/include" "${JULIA_PATH_PREFIX}/include/julia" +) +julia_bail_if_false("Unable to find julia.h. Make sure JULIA_BINDIR is set correctly and that your image is uncompressed." JULIA_INCLUDE_DIR) + +# detect julia version +if(NOT DEFINED JULIA_VERSION) + file(STRINGS "${JULIA_INCLUDE_DIR}/julia_version.h" JULIA_VERSION_LOCAL LIMIT_COUNT 1 REGEX JULIA_VERSION_STRING) + string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" JULIA_VERSION_LOCAL "${JULIA_VERSION_LOCAL}") + set(JULIA_VERSION "${JULIA_VERSION_LOCAL}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Julia + REQUIRED_VARS JULIA_LIBRARY JULIA_EXECUTABLE JULIA_BINDIR JULIA_INCLUDE_DIR + VERSION_VAR JULIA_VERSION +) + +# detect target properties +if(NOT TARGET Julia::Julia) + set(julia_has_implib NO) + set(julia_library_type STATIC) + if(JULIA_LIBRARY MATCHES "\\.(so|dylib)$") + set(julia_library_type SHARED) + elseif(JULIA_LIBRARY MATCHES "\\.(lib|dll\\.a)$") + set(julia_library_type UNKNOWN) + find_file( + JULIA_LIBRARY_DLL + NAMES libjulia.dll julia.dll + HINTS "${JULIA_BINDIR}" + ) + if(JULIA_LIBRARY_DLL) + set(julia_has_implib YES) + set(julia_library_type SHARED) + endif() + endif() + + add_library(Julia::Julia "${julia_library_type}" IMPORTED) + set_target_properties( + Julia::Julia PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${JULIA_INCLUDE_DIR}" + IMPORTED_LINK_INTERFACE_LANGUAGES C + ) + if(julia_has_implib) + if(JULIA_LIBRARY AND EXISTS "${JULIA_LIBRARY}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_IMPLIB "${JULIA_LIBRARY}") + endif() + if(JULIA_LIBRARY_DLL AND EXISTS "${JULIA_LIBRARY_DLL}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_LOCATION "${JULIA_LIBRARY_DLL}") + endif() + elseif(JULIA_LIBRARY AND EXISTS "${JULIA_LIBRARY}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_LOCATION "${JULIA_LIBRARY}") + endif() +endif() + +# finish +mark_as_advanced(JULIA_EXECUTABLE JULIA_BINDIR JULIA_LIBRARY JULIA_INCLUDE_DIR JULIA_VERSION JULIA_LIBRARY_DLL) diff --git a/cmake/install-config.cmake b/cmake/install-config.cmake new file mode 100644 index 0000000..4a2dee1 --- /dev/null +++ b/cmake/install-config.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/jluna-targets.cmake") diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake new file mode 100644 index 0000000..4e647a7 --- /dev/null +++ b/cmake/install-rules.cmake @@ -0,0 +1,78 @@ +# +# Install rules for jluna/CMakeLists.txt +# + +# find_package() call for consumers to find this project +set(package jluna) + +if(PROJECT_IS_TOP_LEVEL) + set(CMAKE_INSTALL_INCLUDEDIR "include/${package}" CACHE PATH "") +endif() + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +install( + FILES jluna.hpp + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT jluna_Development +) + +install( + DIRECTORY include .src + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT jluna_Development +) + +install( + TARGETS jluna jluna_c_adapter + EXPORT jluna-targets + RUNTIME # + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT jluna_Runtime + LIBRARY # + DESTINATION "${CMAKE_INSTALL_LIBDIR}" + COMPONENT jluna_Runtime + NAMELINK_COMPONENT jluna_Development + ARCHIVE # + DESTINATION "${CMAKE_INSTALL_LIBDIR}" + COMPONENT jluna_Development + INCLUDES # + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" +) + +write_basic_package_version_file( + "${package}-config-version.cmake" + COMPATIBILITY SameMajorVersion +) + +# Allow package maintainers to freely override the path for the configs +set( + jluna_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${package}" + CACHE PATH "CMake package config location relative to the install prefix" +) +mark_as_advanced(jluna_INSTALL_CMAKEDIR) + +install( + FILES cmake/install-config.cmake + DESTINATION "${jluna_INSTALL_CMAKEDIR}" + RENAME "${package}-config.cmake" + COMPONENT jluna_Development +) + +install( + FILES "${PROJECT_BINARY_DIR}/${package}-config-version.cmake" + DESTINATION "${jluna_INSTALL_CMAKEDIR}" + COMPONENT jluna_Development +) + +install( + EXPORT jluna-targets + NAMESPACE jluna:: + DESTINATION "${jluna_INSTALL_CMAKEDIR}" + COMPONENT jluna_Development +) + +if(PROJECT_IS_TOP_LEVEL) + include(CPack) +endif() diff --git a/cmake/project-is-top-level.cmake b/cmake/project-is-top-level.cmake new file mode 100644 index 0000000..8bb3807 --- /dev/null +++ b/cmake/project-is-top-level.cmake @@ -0,0 +1,10 @@ +# +# Indicate, whether project() was above was in the top level CMakeLists.txt file +# This variable is set by project() in CMake 3.21+ +# + +string( + COMPARE EQUAL + "${CMAKE_SOURCE_DIR}" "${PROJECT_SOURCE_DIR}" + PROJECT_IS_TOP_LEVEL +) diff --git a/docs/installation.md b/docs/installation.md index 760d7f7..0a36c01 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,269 +1,193 @@ # Creating a Project with jluna -The following is a step-by-step guide to creating an application using `jluna` from scratch. +The following is a step-by-step guide on how to install jluna, and how to create our own C++ application using jluna from scratch. ### Table of Contents -1. [Creating the Project](#creating-the-project) -2. [Setting JULIA_PATH](#setting-julia_path) -3. [Recompiling `jluna`](#building-jluna) -4. [Linking `jluna`](#linking-jluna)
- 4.1 [main.cpp](#linking-jluna)
- 4.2 [CMakeLists.txt](#linking-jluna)
-5. [Troubleshooting](#troubleshooting)
- 5.1 [CMake: Cannot determine location of julia image](#cannot-determine-location-of-julia-image)
- 5.2 [CMake: Cannot find julia.h / libjulia.so](#cannot-find-juliah--libjuliaso)
- 5.3 [C++: Cannot find / ](#cannot-find-juliah--jlunahpp)
- 5.4 [C++: State::initialize fails](#stateinitialize-fails)
-6. [Creating an Issue](#) - -### Creating the Project - -First, we create our workspace directory. For the remainder of this section, this will be assumed to be `~/my_project`. We now execute: +1. [Installing jluna](#installing-jluna)
+ 1.1 [Cloning from Git](#cloning-from-git)
+ 1.2 [Configuring CMake](#configure-cmake)
+ 1.3 [make install](#make-install)
+2. [Troubleshooting](#troubleshooting)
+ 2.1 [Permission Denied](#permission-denied)
+ 2.2 [Unable to Detect Julia Executable](#unable-to-detect-the-julia-executable)
+ 2.3 [Found Unsuitable Version](#could-not-find-julia-found-unsuitable-version)
+ 2.4 [Could NOT find Julia: Missing X](#could-not-find-julia-missing-x)
+ 2.5 [Could not find `julia.h` / `jluna.hpp`](#cannot-find-juliah--jlunahpp)
+ 2.6 [Could not find libjluna_c_adapter](#when-trying-to-initialize-jlunacppcall-cannot-find-libjluna_c_adapter)
+ +## Installing jluna + +This section will guide users on how to install `jluna`, either globally or in a folder-contained manner. + +### Cloning from Git + +We first need to download `jluna`, to do this we navigate into any public folder (henceforth assumed to be `Desktop`) and execute: ```bash -cd ~/my_project -git clone https://github.com/Clemapfel/jluna.git -``` - -This adds the folder `jluna/` to our directory. We now need to compile `jluna`. - -### Setting JULIA_PATH - -To tell `jluna` where to find julia, we need to set the environment variable `JULIA_PATH`, which contains the path to the local julia image. We set it in bash using: - -```bash -export JULIA_PATH=$(julia -e "println(joinpath(Sys.BINDIR, \"..\"))") -``` - -Here, we're calling the julia REPL inline to output the global variable `Sys.BINDIR`, which contains the path to the currently used julia binary. We verify `JULIA_PATH` was set correctly by using: - -```bash -echo $JULIA_PATH -``` -``` -/home/user/Applications/julia/bin/.. -``` -Of course, this path will be different for each user.
Note the prefix `/` which designates an absolute path starting at root and that there is no post-fix `/`. If `JULIA_PATH` reports an empty string, it may be because the `julia` command is not available on a system level. If this is the case, we can simply call the above command from within the julia REPL manually - -```julia -println(joinpath(Sys.BINDIR, "..")) -``` -``` -/path/to/your/julia/bin/.. -``` - -And copy-paste the resulting output to assign `JULIA_PATH` in bash like so: - -```bash -export JULIA_PATH=/path/to/your/julia/bin/.. +git clone https://github.com/clemapfel/jluna.git +cd jluna +mkdir build +cd build ``` -We can now continue to compiling `jluna` using cmake, being sure to stay in the same bash session where we set `JULIA_PATH`. To set `JULIA_PATH` globally, consider consulting [this guide](https://unix.stackexchange.com/questions/117467/how-to-permanently-set-environmental-variables). This way, it does not have to re-set anytime a bash session ends. +This will have cloned the `jluna` git repository into the folder `Desktop/jluna`. -### Building `jluna` +### Configure CMake -With `JULIA_PATH` set correctly, we navigate into `~/my_project/jluna/` and create a new build directory, then call cmake from within that directory: +We now call: ```bash -cd ~/my_project/jluna -mkdir build -cd build -cmake -D CMAKE_CXX_COMPILER=g++-11 .. # or clang++-12 -make +# in Desktop/jluna/build +cmake .. -DCMAKE_CXX_COMPILER= -DCMAKE_INSTALL_PREFIX= ``` -You may need to specify the absolute path to `g++-11` / `clang-12` if their respective executables are not available in the default search paths. +Where `` is one of: ++ `g++-10` ++ `g++-11` ++ `clang++-12` -When compiling, warnings of the following type may appear: +And `` is the desired install path. `-DCMAKE_INSTALL_PREFIX=` is optional, if it is specified manually (not recommended), keep note of this path as we will need it later. -``` -(...) -/home/user/Applications/julia/bin/../include/julia/julia_locks.h:72:32: warning: ‘++’ expression of ‘volatile’-qualified type is deprecated [-Wvolatile] - 72 | jl_current_task->ptls->defer_signal++; \ -(...) -``` -This is because the official julia header `julia.h` is slightly out of date. The warning is unrelated to `jluna`s codebase, `jluna` itself should report no warnings or errors. If this is not the case, head to [troubleshooting](#troubleshooting). +> Window supports is experimental. This means using MSVC may work, however this is currently untested. The [cpp compiler support]() page seems to imply that MSVC 19.30 or newer is required to compile `jluna`. -We verify everything works by running `JLUNA_TEST` which we just compiled: +Some errors may appear here, if this is the case, head to [troubleshooting](#troubleshooting). -```bash -# in ~/my_project/jluna/build/ -./JLUNA_TEST -``` - -A lot of output will appear. At the very end it should show: +If the command does not recognize the compiler, even though you are sure it is installed, it may be necessary to specify the full path to the compiler executable instead, like so: ``` -Number of tests unsuccessful: 0 +-DCMAKE_CXX_COMPILER=/usr/bin/g++-10 ``` -This means everything works! +### Make Install -We then clean up the build files using: +Having successfully configured cmake, we now call: ```bash -# in ~/my_project/jluna/build -cd .. -rm -r build +# in Desktop/jluna/build +make install ``` -We have now compiled and verified `jluna` and are left with a shiny new `libjluna.so` and `libjluna_c_adapter.so` in `~/my_project/jluna/`. These are the shared libraries that contain `jluna`s functionality. - -Advanced users can stop this tutorial now and simply link their library against `libjluna.so`, `libjluna_c_adapter.so` (both in '`~/my_project/jluna/`) and against `libjulia.so`, which is in the directory `$ENV{JULIA_PATH}/lib/`. - - -For people who aren't yet as familiar with cmake and linking, let's continue: +Which will create two shared libraries `libjluna.*`, and `libjluna_c_adapter.*`, where `*` is the platform-dependent library suffix. -### Linking `jluna` +Now that `jluna` is installed on our system, we can access it using: -We now need to create our own application. First we create a `main.cpp`: - -```bash -cd ~/my_project -gedit main.cpp +```cmake +# in users own CMakeLists.txt +find_library(jluna REQUIRED) ``` -This will open a GUI editor, if `gedit` is unavailable, any other editor (`vim`, `nano`, `emacs`, etc.) can be used. - -We replace the contents of `main.cpp` with the following: - -```cpp -#include - -using namespace jluna; +If a custom install directory was specified, we need to make cmake aware of this: -int main() -{ - State::initialize(); - Base["println"]("hello julia"); -} +```cmake +# in users own CMakeLists.txt +find_library(jluna REQUIRED + NAMES jluna + PATHS +) ``` -then safe and close the file. +Where `` is the path specified as `-DCMAKE_INSTALL_PREFIX` during configuration before. -To compile our project, we again use cmake. We first create `CMakeLists.txt`: +Note that `jlunas` install rules also export is as a package, so it will be available through `find_package` on systems where it was installed globally like so: -```bash -# in ~/my_project/ -gedit CMakeLists.txt +```cmake +find_package(jluna REQUIRED) ``` -And replace its contents with the following: +After `find_library` or `find_package`, we link our own library like so: ```cmake -cmake_minimum_required(VERSION 3.16) - -# name of our project -project(MyProject) - -# julia -if (NOT DEFINED ENV{JULIA_PATH}) - message(WARNING "JULIA_PATH was not set correctly. Consider re-reading the jluna installation tutorial at https://github.com/Clemapfel/jluna/blob/master/docs/installation.md#setting-julia_path to fix this issue") -endif() +# in users own CMakeLists.txt +target_link_libraries(my_library PRIVATE + "${jluna}" + "${}" +) +target_include_directories(my_library PRIVATE + "${}" + "${}" +) +``` + +Where ++ `` is the folder containing `jluna.hpp` ++ `` is the library/package containing the julia C-API ++ `` is the folder containing `julia.h`. + +The shared julia library location is usually ++ `${JULIA_BINDIR}/../lib` + +while the julia include directory is usually ++ `${JULIA_BINDIR}/../include/` or ++ `${JULIA_BINDIR}/../include/julia/` + +If building your library triggers linker or compiler errors, head to [troubleshoot](#troubleshooting). -set(JULIA_INCLUDE "$ENV{JULIA_PATH}/include/julia") - -set(JULIA_LIB "$ENV{JULIA_PATH}/lib/libjulia.so") +--- -find_package(jluna REQUIRED) +## Troubleshooting -# add our executable -add_executable(MY_EXECUTABLE ${CMAKE_SOURCE_DIR}/main.cpp) +### Permission Denied -target_include_directories(MY_EXECUTABLE PRIVATE ${JULIA_INCLUDE}) +During `make install`, or during execution of the bash script, your OS may notify you that it was unable to write to a folder due to missing permissions. To fix this, either run `make install` as `sudo` (or as administrator on windows), or specify a different folder using `-DCMAKE_INSTALL_PREFIX` for which `jluna` or cmake does have write/read permission. -# link executable with jluna, jluna_c_adapter and julia -target_link_libraries(MY_EXECUTABLE jluna::jluna ${JULIA_LIB}) -``` +### Unable to detect the Julia executable -We again save and close the file, then create our own build folder and run cmake, just like we did with `jluna` before +When calling: ```bash -# in ~/my_project -mkdir build -cd build -cmake -D CMAKE_CXX_COMPILER=g++-11 .. -make +# in ~/Desktop/jluna/build +cmake .. -DCMAKE_COMPILER=g++-10 # or other compiler ``` -If errors appear, be sure `JULIA_PATH` is still set correctly, as it is needed to find `julia.h`. Otherwise, head to [troubleshooting](#troubleshooting). - -After compilation succeeded, the directory should now have the following layout: +You may encounter the following error: ``` -my_project/ - CMakeLists.txt - main.cpp - jluna/ - libjluna.so - libjluna_c_adapter.so - jluna.hpp - (...) - build/ - MY_EXECUTABLE - (...) +CMake Error at cmake/find/FindJulia.cmake:5 (message): + Unable to detect the julia executable. Make sure JULIA_BINDIR is set + correctly. ``` -Where names with a `/` suffix are folders. -We can now run our application using: +This error appears, because `jluna` was unable to locate the julia package on your system. To make `jluna` aware of the location manually, we can pass the following variable to the cmake command: ```bash -# in ~/my_project/build -./MY_EXECUTABLE -``` +cmake .. -DJULIA_BINDIR=path/to/your/julia/bin -DCMAKE_COMPILER=g++-10 ``` -[JULIA][LOG] initialization successfull. -hello julia -``` - -`State::initialize()` may fail. If this is the case, head to [troubleshooting](#troubleshooting). Otherwise, congratulations! We are done and can now start developing our own application with the aid of julia and `jluna`. ---- - -## Troubleshooting - -### Cannot determine location of julia image - -When compiling `jluna`, the following error may occur: +Where `path/to/your/julia/bin` is the path of the binary directory of your julia image. If you are unsure of its location, you can execute +```julia +println(Sys.BINDIR) ``` - Cannot determine location of julia image. Before running cmake, please - manually set the environment variable JULIA_PATH using - export JULIA_PATH=/path/to/your/.../julia +From inside the julia REPL. - If you are unsure of the location of your julia image, you can access the - path from within the julia REPL using +### Found unsuitable version - println(joinpath(Sys.BINDIR, "..")) +During the cmake configuration step, the following error may appear: - For more information, visit - https://github.com/Clemapfel/jluna/blob/master/README.md#troubleshooting +``` +CMake Error at /home/(...)/FindPackageHandleStandardArgs.cmake:218 (message): + Could NOT find Julia: Found unsuitable version "1.5.1", but required is at + least "1.7.0" (found /home/clem/Applications/julia/lib/libjulia.so) ``` -This is because `JULIA_PATH` is not properly set. Repeat the section on [setting `JULIA_PATH`](#setting-julia_path) to resolve this issue. - -### Cannot find julia.h / libjulia.so - -When building `jluna`, the following warnings may appear: +Where `1.5.1` could instead be any version before `1.7.0`. This means your julia version is out of date, either update it through your packet manager or download the latest version [here](https://julialang.org/downloads/), install it, then make sure `JULIA_BINDIR` is pointing to the newer version. -``` -CMake Warning at CMakeLists.txt:37 (message): - Cannot find library header julia.h in (...) -``` +### Could NOT find Julia (missing: X) -This means verification of the directory specified through `JULIA_PATH` failed. We can make sure the path is correct by verifying: +Where X can be any of : ++ `JULIA_LIBRARY` ++ `JULIA_EXECUTABLE` ++ `JULIA_BINDIR` ++ `JULIA_INCLUDE_DIR` -+ the path is an absolute path starting at root -+ the path has a prefix `/` -+ the path has no suffix `/` -+ the path is the same as reported in the julia REPL +This means that either `JULIA_BINDIR` was not set correctly or the directory it is pointing to is not the julia binary directory. Verify that the value of `JULIA_BINDIR` starts at root (`/` on unix and `C:/` on windows), ends in `/bin`, and that your julia image folder is uncompressed. -If all of these are true, it may be that your julia image is corrupted or compressed. The julia folder should have the following layout: +Make sure the folder `JULIA_BINDIR` points to, has the following layout: ``` -julia/ +julia*/ bin/ julia include/ @@ -276,7 +200,8 @@ julia/ (...) ``` -Where names with a suffix `/` are folders. If your julia folder looks different, redownload the latest official release [here](https://julialang.org/downloads/#current_stable_release) and reassign `JULIA_PATH` accordingly. + +Where `*` may be a version suffix, such as `julia-1.7.2`. ### Cannot find / @@ -298,97 +223,60 @@ fatal error: jluna.hpp: No such file or directory compilation terminated. ``` -This means the `include_directories` in cmake where set improperly. Make sure the following lines are present in your `CMakeLists.txt`: +This means the `include_directories` in your `CMakeLists.txt` were set improperly. Make sure the following lines are present in your `CMakeLists.txt`: ``` -include_directories("${CMAKE_SOURCE_DIR}/jluna") -include_directories("$ENV{JULIA_PATH}/include/julia") +target_include_directories( PRIVATE + "" + "" +) ``` +Where -Furthermore, make sure your directory has the following structure: ++ is the build target, a library or executable ++ `` is the install path of the `jluna` shared libary, possibly specified during cmake configuration using `-DCMAKE_INSTALL_PREFIX` ++ `` is the location of `julia.h`, usually `${JULIA_BINDIR}/../include` or `${JULIA_BINDIR}/../include/julia` -``` -my_project/ - main.cpp - CMakeLists.txt - jluna/ - libjluna.so - libjluna_c_adapter.so - jluna.hpp - include/ - (...) - (...) - build/ - (...) -``` +### Cannot find libjluna_c_adapter -And make sure `JULIA_PATH` is set correctly (see above). +When calling -### State::initialize fails - -#### AssertionError("jluna requires julia v1.7.0 or higher") +```cpp +State::initialize() +``` -`jluna` asserts wether the correct version of julia is present on initialization. If the following exception occurs when calling `State::initialize`: +The following error may appear: ``` +AssertionError: when trying to initialize jluna.cppcall: cannot find /home/Desktop/jluna/libjluna_c_adapter.so +Stacktrace: + [1] verify_library() + @ Main.jluna._cppcall ./none:951 + [2] top-level scope + @ none:2 +[JULIA][ERROR] initialization failed. terminate called after throwing an instance of 'jluna::JuliaException' - what(): [JULIA][EXCEPTION] AssertionError("jluna requires julia v1.7.0 or higher, but v1.6.2 was detected. Please download the latest julia release at https://julialang.org/downloads/#current_stable_release, set JULIA_PATH accordingly, then recompile jluna using cmake. For more information, visit https://github.com/Clemapfel/jluna/blob/master/README.md#troubleshooting") + what(): [JULIA][EXCEPTION] AssertionError("[JULIA][ERROR] initialization failed.") signal (6): Aborted -in expression starting at none:0 -(...) ``` -It means that your julia version is out of date. [Download the latest version](https://julialang.org/downloads/#current_stable_release), set `JULIA_PATH` to point to it, then recompile `jluna` as outlined [above](#building-jluna). +To allow the local julia state to interface with `jluna`, it needs the shared C-adapter-library to be available. During `make install`, `jluna` modifies its own code to keep track of the location of the C-adapter. If it was moved, `jluna` may no longer be able to find it. +To fix this, recompile jluna, as detailed [above](#make-install). -### ERROR: could not load library -When calling `State::initialize`, julias C-API may report an error of the following type: - -``` -ERROR: could not load library "(...)/lib/julia/sys.so" -(...)/lib/julia/sys.so: cannot open shared object file: No such file or directory -``` +The C-adapter library is always installed into the directory specified by `CMAKE_INSTALL_PREFIX`, regardless of cmake presets used. Be aware of this. -This can mean three things +> **HACK**: Some IDEs and modern versions of cmake may override `CMAKE_INSTALL_PREFIX` between the time of configuration and build. As a hacky fix (March, 2022), you can override the C-adapter shared library location manually, **before calling `State::initialize`**, using `State::set_c_adapter_path`: +> ```cpp +> int main() +> { +> State::set_c_adapter_path("C:/actual/path/to/jluna_c_adapter.dll") +> State::initialize(); +> (...) +> ``` +> A future release of `jluna` will provide a proper solution for this. -+ A) `JULIA_PATH` was set incorrectly (see above) -+ B) your executable `MY_EXECUTABLE` does not have read permission for the folders in `JULIA_PATH` -+ C) julia is not installed on a system level - -If C is true, replace `State::initialize()` with its overload: +--- -```cpp -State::initialize("/path/to/(...)/julia/bin") -``` -Where `/path/to/(...)` is replaced with the absolute path to your image of julia. This will tell the C-API to load julia from that image, rather than from the default system image. - -## Creating an Issue - -If your problem still persists, it may be appropriate to open a github issue. First, please verify that: - -+ you are on a linux-based, 64-bit operating system -+ julia 1.7.0 (or higher) is installed and no older version exists on your machine -+ cmake 3.16 (or higher) is installed and no older version exists on your machine -+ g++-10 or g++-11 is installed -+ gcc-9 (or higher) is installed -+ when building with cmake, you specified `-D CMAKE_CXX_COMPILER=g++-11` correctly - - cmake is able to find the executable `g++-11` in the default search paths - - the same applies to `-D CMAKE_C_COMPILER=gcc-9` as well - - any given issue may be compiler specific, consider trying `clang-12` instead -+ `~/my_project/CMakeLists.txt` is identical to the code in [installation](#linking-jluna) -+ `State::initialize` and `JULIA_PATH` are modified as outlined [above](#stateinitialize-fails) -+ `jluna` was freshly pulled from the git repo and recompiled -+ `~/my_project/jluna/` contains `libjluna.so` and `libjluna_c_adapter.so` -+ `~/my_project/jluna/build/JLUNA_TEST` was ran -+ your issue is not otherwise covered in [troubleshooting](#troubleshooting), or any other already resolved issue - -If and only if all of the above apply, head to the [issues tab](https://github.com/Clemapfel/jluna/issues). There, please create an issue and -+ describe your problem -+ state your operating system, distro and compiler version -+ copy-paste the output of `JLUNA_TEST` (even if all tests are `OK`) -+ provide a minimum working example of your project that recreates the bug - -We will try to resolve your problem as soon as possible and are thankful for your input and collaboration. - ---- \ No newline at end of file +If your particular problem was not addressed in this section, feel free to [open an issue on GitHub](https://github.com/Clemapfel/jluna/issues). diff --git a/include/array.hpp b/include/array.hpp index ea16890..17dd8fa 100644 --- a/include/array.hpp +++ b/include/array.hpp @@ -18,10 +18,10 @@ namespace jluna template class Array : public Proxy { - friend class ConstIterator; - class Iterator; - public: + friend class ConstIterator; + class Iterator; + /// @brief value type using value_type = Value_t; @@ -143,6 +143,7 @@ namespace jluna void throw_if_index_out_of_range(int index, size_t dimension) const; size_t get_dimension(int) const; + public: class ConstIterator { public: @@ -173,16 +174,15 @@ namespace jluna /// @returns bool bool operator!=(const ConstIterator&) const; - /// @brief decays into value_type - template - T operator*() const; - /// @brief forward to self + auto operator*() const; + + /// @brief forward to assignable auto operator*(); /// @brief decay into unboxed value /// @tparam value-type, not necessarily the same as declared in the array type - template, bool> = true> + template, bool> = true> operator T() const; /// @brief decay into proxy @@ -318,5 +318,5 @@ namespace jluna } } -#include ".src/array.inl" -#include ".src/array_iterator.inl" \ No newline at end of file +#include <.src/array.inl> +#include <.src/array_iterator.inl> \ No newline at end of file diff --git a/include/box.hpp b/include/box.hpp index be4ee3d..694ff25 100644 --- a/include/box.hpp +++ b/include/box.hpp @@ -11,7 +11,7 @@ #include #include -#include +#include #include #include @@ -197,4 +197,4 @@ namespace jluna }; } -#include ".src/box.inl" \ No newline at end of file +#include <.src/box.inl> \ No newline at end of file diff --git a/include/concepts.hpp b/include/concepts.hpp index 78f3514..7244593 100644 --- a/include/concepts.hpp +++ b/include/concepts.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include #include diff --git a/include/cppcall.hpp b/include/cppcall.hpp index b34a5ae..f768d5d 100644 --- a/include/cppcall.hpp +++ b/include/cppcall.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include @@ -87,4 +87,4 @@ namespace jluna Function* register_unnamed_function(const Lambda_t& lambda); } -#include ".src/cppcall.inl" \ No newline at end of file +#include <.src/cppcall.inl> \ No newline at end of file diff --git a/include/exceptions.hpp b/include/exceptions.hpp index 610197a..81fa036 100644 --- a/include/exceptions.hpp +++ b/include/exceptions.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -82,4 +82,4 @@ namespace jluna Any* operator""_eval(const char*, size_t); } -#include ".src/exceptions.inl" \ No newline at end of file +#include <.src/exceptions.inl> \ No newline at end of file diff --git a/include/gc_sentinel.hpp b/include/gc_sentinel.hpp index a040c18..e1aba71 100644 --- a/include/gc_sentinel.hpp +++ b/include/gc_sentinel.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include namespace jluna { diff --git a/include/jluna.jl b/include/jluna.jl deleted file mode 100644 index 6fa101f..0000000 --- a/include/jluna.jl +++ /dev/null @@ -1,1053 +0,0 @@ -# -# Copyright 2021 Clemens Cords -# Created on 26.12.2021 by clem (mail@clemens-cords.com) -# - -""" -module with julia-functionality needed by the C++ side of jluna. -Most end-user should not call any function in this module, with `cppcall` being the only exception -""" -module jluna - - """ - `get_value_type_of_array(::Array{T}) -> Type` - - forward value type of array - """ - function get_value_type_of_array(_::Array{T}) ::Type where T - - return T - end - - """ - `get_reference_value(::Base.RefValue{T}) -> T` - - forward value of reference - """ - function get_reference_value(ref::Base.RefValue{T}) ::T where T - - return ref[]; - end - - setindex!(str::String, c::Char, i::Int64) = setindex!(collect(str), c, i) - - """ - `is_number_only(::String) -> Bool` - - check whether a string can be transformed into a base 10 number - """ - function is_number_only(x::String) ::Bool - - for s in x - if s != '0' || s != '1' || s != '2' || s != '3' || s != '4' || s != '5' || s != '6' || s != '7' || s != '8' || s != '9' - return false - end - end - - return true; - end - - """ - `is_method_available(::Function, ::Any) -> Bool` - - check if method of given function is available for a specific variable - """ - function is_method_available(f::Function, variable) ::Bool - - return hasmethod(f, Tuple{typeof(variable)}) - end - - """ - `get_function(::Symbol, ::Module) -> Function` - - exception-safe function access wrapper - """ - function get_function(function_name::Symbol, m::Module) ::Function - - return m.eval(function_name) - end - export get_function - - """ - `exists(<:AbstractArray, ::Any) -> Bool` - - check if element exists in array - """ - function exists(array::T, v::Any) ::Bool where T <: AbstractArray - - return !isempty(findall(x -> x == v, array)) - end - - """ - `tuple_at(::Tuple, ::Integer) -> Any` - - get nth element of tuple - """ - function tuple_at(x::Tuple, i::Integer) - return x[i] - end - - """ - `tuple_length(::Tuple) -> Integer` - - get length of tuple - """ - function tuple_length(x::Tuple{N}) where N - return N - end - - """ - `make_new(::String, xs...) -> Any` - - parse string to type, then call ctor with given args - """ - function make_new(name::String, xs...) ::Any - - return Main.eval(:($(Meta.parse(name))($(xs...)))) - end - - """ - `make_vector(::T...) -> Vector{T}` - - wrap vector ctor in varargs argument, used by box/unbox - """ - function make_vector(args::T...) ::Vector{T} where T - - return [args...] - end - - """ - `make_vector(t::Type) -> Vector{t}` - - create empty vector of given type - """ - function make_vector(t::Type) ::Vector{t} - - return Vector{t}() - end - - """ - `make_vector(::Type{T}, ::Any...) -> Vector{T}` - - create vector by converting all elements to target type - """ - function make_vector(type::Type{T}, xs...) where T - return convert.(T, [xs...]) - end - - """ - `new_vector(::Integer, ::T) -> Vector{T}` - - create vector by deducing argument type - """ - function new_vector(size::Integer, _::T) where T - return Vector{T}(undef, size) - end - - """ - `new_vector(::Integer, ::T) -> Vector{T}` - - create vector by deducing argument type - """ - function new_vector(size::Integer, type::Type) - return Vector{type}(undef, size) - end - - """ - `make_set(::T...) -> Set{T}` - - wrap set ctor in varargs argument, used by box/unbox - """ - function make_set(args::T...) ::Set{T} where T - - return Set([args...]); - end - - """ - `make_set(::Type{T}, ::Any...) -> Set{T}` - - - """ - function make_set(type::Type{T}, xs...) ::Set{T} where T - - return Set(make_vector(type, xs...)); - end - - """ - `make_pair(::T, ::U) -> Pair{T, U}` - - wrap pair ctor - """ - function make_pair(a::T, b::U) ::Pair{T, U} where {T, U} - - return a => b - end - - """ - `make_complex(:T, :T) -> Complex{T}` - - wrap complex ctor - """ - function make_complex(real::T, imag::T) ::Complex{T} where T - return Complex{T}(real, imag) - end - - """ - `assert_isa(::T, ::Symbol) -> Nothing` - - throw assertion if x is not of named type - """ - function assert_isa(x::Any, type::Type) ::Nothing - - if !(x isa type) - throw(AssertionError("expected " * string(type) * " but got an object of type " * string(typeof(x)))); - end - - return nothing - end - - """ - `convert(::T, symbol::Symbol) -> Any` - - convert value type, declared via symbol - """ - function convert(x::T, symbol::Symbol) ::Any where T - - type = Main.eval(symbol); - @assert type isa Type - - if T isa type - return T - end - - return Base.convert(type, x) - end - - """ - `string_to_type` -> Type - - evaluate string to type name if possible - """ - function string_to_type(str::String) ::Type - - res = Main.eval(Meta.parse(str)) - if !(res isa Type) - throw(UndefVarError(Symbol(str))) - end - return res - end - - """ - `invoke(function::Any, arguments::Any...) -> Any` - - wrap function call for non-function objects - """ - function invoke(x::Any, args...) ::Any - return x(args...) - end - - """ - `create_or_assign(::Symbol, ::T) -> T` - - assign variable in main, or if none exist, create it and assign - """ - function create_or_assign(symbol::Symbol, value::T) ::T where T - - return Main.eval(Expr(:(=), symbol, value)) - end - - """ - `serialize(<:AbstractDict{T, U}) -> Vector{Pair{T, U}}` - - transform dict into array - """ - function serialize(x::T) ::Vector{Pair{Key_t, Value_t}} where {Key_t, Value_t, T <: AbstractDict{Key_t, Value_t}} - - out = Vector{Pair{Key_t, Value_t}}() - for e in x - push!(out, e) - end - return out; - end - - """ - `serialize(::Set{T}) -> Vector{T}` - - transform dict into array - """ - function serialize(x::T) ::Vector{U} where {U, T <: AbstractSet{U}} - - out = Vector{U}() - - for e in x - push!(out, e) - end - - return out; - end - - """ - `dot(::Array, field::Symbol) -> Any` - - wrapped dot operator - """ - function dot(x::Array, field_name::Symbol) ::Any - - index_maybe = parse(Int, string(field_name)); - @assert index_maybe isa Integer - return x[index_maybe]; - end - export dot; - - """ - `dot(::Module, field::Symbol) -> Any` - - wrapped dot operator - """ - dot(x::Module, field_name::Symbol) = return x.eval(field_name); - - """ - `dot(x::Any, field::Symbol) -> Any` - - wrapped dot operator, x.field - """ - dot(x::Any, field_name::Symbol) = return eval(:($x.$field_name)) - - """ - `unroll_type(::Type) -> Type` - - unroll type declaration - """ - function unroll_type(type::Type) ::Type - - while hasproperty(type, :body) - type = type.body - end - - return type - end - - """ - `is_name_typename(::Type, ::Type) -> Bool` - - unroll type declaration, then check if name is typename - """ - function is_name_typename(type_in::Type, type_comparison::Type) ::Bool - return getproperty(type_in, :name) == Base.typename(type_comparison) - end - - """ - `get_n_fields(::Type) -> Int64` - """ - function get_n_fields(type::Type) ::Int64 - return length(fieldnames(type)) - end - - """ - `get_fields(::Type) -> Vector{Pair{Symbol, Type}}` - - get field symbols and types, used by jluna::Type::get_fields - """ - function get_fields(type::Type) ::Vector{Pair{Symbol, Type}} - - out = Vector{Pair{Symbol, Type}}(); - names = fieldnames(type) - types = fieldtypes(type) - - for i in 1:(length(names)) - push!(out, names[i] => types[i]) - end - - return out - end - - """ - `get_parameter(::Type) -> Vector{Pair{Symbol, Type}}` - - get parameter symbols and upper type limits, used by jluna::Type::get_parameters - """ - function get_parameters(type::Type) ::Vector{Pair{Symbol, Type}} - - type = unroll_type(type) - - out = Vector{Pair{Symbol, Type}}(); - parameters = getproperty(type, :parameters) - - for i in 1:(length(parameters)) - push!(out, parameters[i].name => parameters[i].ub) - end - - return out - end - - """ - `get_n_parameters(::Type) -> Int64` - """ - function get_n_parameters(type::Type) ::Int64 - - type = unroll_type(type) - - return length(getproperty(type, :parameters)) - end - - """ - `assign_in_module(::Module, ::Symbol, ::T) -> T` - - assign variable in other module, throws if variable does not exist - """ - function assign_in_module(m::Module, variable_name::Symbol, value::T) ::T where T - - if (!isdefined(m, variable_name)) - throw(UndefVarError(Symbol(string(m) * "." * string(variable_name)))) - end - - return Base.eval(m, :($variable_name = $value)) - end - - """ - `create_in_module(::Module, ::Symbol, ::T) -> T` - - assign variable in other module, if variable does not exist, create then assign - """ - function create_or_assign_in_module(m::Module, variable_name::Symbol, value::T) ::T where T - return Base.eval(m, :($variable_name = $value)) - end - - """ - `get_names(::Module) -> IdDict{Symbol, Any}` - - access all module members as dict - """ - function get_names(m::Module) ::IdDict{Symbol, Any} - - out = IdDict{Symbol, Any}() - - for n in names(m; all = true) - if string(n)[1] != '#' - out[n] = m.eval(n) - end - end - - return out - end - - """ - `get_nth_method(::Function, ::Integer) -> Method` - - wrap method access, used by jlune::Method - """ - function get_nth_method(f::Function, i::Integer) ::Method - - return methods(f)[i] - end - - """ - `get_return_type_of_nth_method(::Function, ::Integer) -> Type` - - used by jluna::Function to deduce method signature - """ - function get_return_type_of_nth_method(f::Function, i::Integer) ::Type - - return Base.return_types(test)[i] - end - - """ - `get_argument_type_of_nths_methods(::Function, ::Integer) -> Vector{Type}` - - used by jluna::Function to deduce method signature - """ - function get_argument_types_of_nth_method(f::Function, i::Integer) ::Vector{Type} - - out = Vector{Type}() - types = methods(f)[i].sig.types - - for i in 2:length(types) - push!(out, types[i]) - end - - return out - end - - - """ - `get_length_of_generator(::Base.Generator) -> Int64` - - deduce length of Base.Generator object - """ - function get_length_of_generator(gen::Base.Generator) ::Int64 - - if (Base.haslength(gen)) - return length(gen) - else - # heuristically deduce length - for i in Iterators.reverse(gen.iter.itr) - if gen.iter.flt(i) - return i - end - end - end - end - - """ - offers verbose exception interface. Any call with safe_call will store - the last exception and full stack trace as string in _last_exception and - _last_message respectively - """ - module exception_handler - - """ - exception Placeholder that signals that no exception occurred - """ - struct NoException <: Exception end - export NoException - - """ - current state of the exception handler - - _last_exception <:Exception - _last_message ::String - """ - mutable struct State - _last_exception - _last_message::String - _exception_occurred::Bool - end - - const _state = Ref{State}(State(NoException(), "", false)); - - """ - `safe_call(::Function, ::Any...) -> Any` - - call any function, update the handler then forward the result, if any - """ - function safe_call(f::Any, args...) - - result = undef - try - result = f(args...) - update() - catch exc - result = nothing - update(exc) - end - - return result; - end - - """ - `safe_call(::Expr, ::Module = Main) -> Any` - - call any line of code, update the handler then forward the result, if any - """ - function safe_call(expr::Expr, m::Module = Main) ::Any - - if expr.head == :block - expr.head = :toplevel - end - - return safe_call(Base.eval, m, expr); - end - - """ - `unsafe_call(::Expr, ::Module = Main) -> Any` - - call any line of code without updating the handler - """ - function unsafe_call(expr::Expr, m::Module = Main) ::Any - - if expr.head == :block - expr.head = :toplevel - end - - return Base.eval(m, expr); - end - - """ - `update(<:Exception) -> Nothing` - - update the handler after an exception was thrown - """ - function update(exception::Exception) ::Nothing - - try - global _state[]._last_message = sprint(Base.showerror, exception, catch_backtrace()) - global _state[]._last_exception = exception - global _state[]._exception_occurred = true - catch e end - return nothing - end - - """ - `update() -> Nothing` - - update the handler after *no* exception was thrown - """ - function update() ::Nothing - - if !(_state[]._last_exception isa NoException) - - global _state[]._last_message = "" - global _state[]._last_exception = NoException() - global _state[]._exception_occurred = false - end - return nothing - end - - """ - `has_exception_occurred() -> Bool` - - is last exception type no "jluna.exception_handler.NoException" - """ - function has_exception_occurred() ::Bool - - return _state[]._exception_occurred - end - - """ - `get_last_message() -> String` - - wrapper for _state access - """ - function get_last_message() ::String - return _state[]._last_message - end - - """ - `get_last_exception() -> Exception` - - wrapper for _state access - """ - function get_last_exception() ::Exception - return _state[]._last_exception - end - end - - """ - offers julia-side memory management for C++ jluna - """ - module memory_handler - - const _current_id = Ref(UInt64(0)); - const _refs = Ref(Dict{UInt64, Base.RefValue{Any}}()) - const _ref_counter = Ref(Dict{UInt64, UInt64}()) - - const _ref_id_marker = '#' - const _refs_expression = Meta.parse("jluna.memory_handler._refs[]") - - # proxy id that is actually an expression, the ID of topmodule Main is - ProxyID = Union{Expr, Symbol, Nothing} - - # make as unnamed - make_unnamed_proxy_id(id::UInt64) = return Expr(:ref, Expr(:ref, _refs_expression, id)) - - # make as named with owner and symbol name - make_named_proxy_id(id::Symbol, owner_id::ProxyID) ::ProxyID = return Expr(:(.), owner_id, QuoteNode(id)) - - # make as named with main as owner and symbol name - make_named_proxy_id(id::Symbol, owner_id::Nothing) ::ProxyID = return id - - # make as named with owner and array index name - make_named_proxy_id(id::Number, owner_id::ProxyID) ::ProxyID = return Expr(:ref, owner_id, convert(Int64, id)) - - # assign to proxy id - 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) - evaluate(name::Symbol) ::Any = return Main.eval(:($name)) - - """ - `get_name(::ProxyID) -> String` - - parse name from proxy id - """ - function get_name(id::ProxyID) ::String - - if length(id.args) == 0 - return "Main" - end - - current = id - while current.args[1] isa Expr && length(current.args) >= 2 - - if current.args[2] isa UInt64 - current.args[2] = convert(Int64, current.args[2]) - end - - current = current.args[1] - end - - out = string(id) - reg = r"\Q((jluna.memory_handler._refs[])[\E(.*)\Q])[]\E" - captures = match(reg, out) - - if captures != nothing - out = replace(out, reg => "") - end - - return out; - end - - get_name(::Nothing) ::String = return "Main" - get_name(s::Symbol) ::String = return string(s) - get_name(i::Integer) ::String = return "[" * string(i) * "]" - - """ - `print_refs() -> Nothing` - - pretty print _ref state, for debugging - """ - function print_refs() ::Nothing - - println("jluna.memory_handler._refs: "); - for e in _refs[] - println("\t", Int64(e.first), " => ", e.second[], " (", typeof(e.second[]), ")") - end - end - - """ - `create_reference(::UInt64, ::Any) -> UInt64` - - add reference to _refs - """ - function create_reference(to_wrap::Any) ::UInt64 - - global _current_id[] += 1; - key::UInt64 = _current_id[]; - - if (haskey(_refs[], key)) - _ref_counter[][key] += 1 - else - _refs[][key] = Base.RefValue{Any}(to_wrap) - _ref_counter[][key] = 1 - end - - return key; - end - - create_reference(_::Nothing) ::UInt64 = return 0 - - """ - `set_reference(::UInt64, ::T) -> Nothing` - - update the value of a reference in _refs without adding a new entry or changing it's key, ref pointers C++ side stay valid - """ - function set_reference(key::UInt64, new_value::T) ::Base.RefValue{Any} where T - - _refs[][key] = Base.RefValue{Any}(new_value) - return _refs[][key] - end - - """ - `get_reference(::Int64) -> Any` - - access reference in _refs - """ - function get_reference(key::UInt64) ::Any - - if (key == 0) - return nothing - end - - return _refs[][key] - end - - """ - `free_reference(::UInt64) -> Nothing` - - free reference from _refs - """ - function free_reference(key::UInt64) ::Nothing - - if (key == 0) - return nothing; - end - - if _refs[][key][] isa Module - return - end - - global _ref_counter[][key] -= 1 - count::UInt64 = _ref_counter[][key] - - if (count == 0) - delete!(_ref_counter[], key) - delete!(_refs[], key) - end - - return nothing; - end - - """ - `force_free() -> Nothing` - - immediately deallocate all C++-managed values - """ - function force_free() ::Nothing - - for k in keys(_refs) - delete!(_refs[], k) - delete!(_ref_counter[], k) - end - - return nothing; - end - end - - module _cppcall - - mutable struct State - _arguments::Tuple - _result::Any - - State() = new((), nothing) - end - - _library_name = "" - _state = Base.Ref{_cppcall.State}(State()) - - """ - Wrapper object for unnamed functions, frees function once object is destroyed - """ - mutable struct UnnamedFunctionProxy{N, Return_t} - - _id::Symbol - _call::Function - _n_args::Int64 - - function UnnamedFunctionProxy{N, Return_t}(id::Symbol) where {N, Return_t} - - @assert(-1 <= N <= 4) - - _id = id - x = new{N, Return_t}(id, function (xs...) Main.cppcall(_id, xs...) end, N) - - finalizer(function (t::UnnamedFunctionProxy{N, Return_t}) - ccall((:free_function, _cppcall._library_name), Cvoid, (Csize_t,), hash(t._id)) - end, x) - - return x - end - end - - # call with signature (1x Any) -> [Any / Nothing] - function (instance::UnnamedFunctionProxy{0, T})() ::T where T - return instance._call(); - end - - # call with signature (1x Any) -> [Any / Nothing] - function (instance::UnnamedFunctionProxy{1, T})(arg1) ::T where T - return instance._call(arg1); - end - - # call with signature (2x Any) -> [Any / Nothing] - function (instance::UnnamedFunctionProxy{2, T})(arg1, arg2) ::T where T - return instance._call(arg1, arg2); - end - - # call with signature (3x Any) -> [Any / Nothing] - function (instance::UnnamedFunctionProxy{3, T})(arg1, arg2, arg3) ::T where T - return instance._call(arg1, arg2, arg3); - end - - # call with signature (4x Any) -> [Any / Nothing] - function (instance::UnnamedFunctionProxy{4, T})(arg1, arg2, arg3, arg4) ::T where T - return instance._call(arg1, arg2, arg3, arg4); - end - - # call with other signature - function (instance::UnnamedFunctionProxy{Int64(-1), T})(args::Vector) ::T where T - return instance._call(args...); - end - - # ctor wrapper for jluna - new_unnamed_function(s::Symbol, N::Integer, T::Type) = return UnnamedFunctionProxy{N, T}(s) - - """ - an exception thrown when trying to invoke cppcall with a function name that - has not yet been registered via jluna::register_function - """ - mutable struct UnregisteredFunctionNameException <: Exception - - _function_name::Symbol - end - Base.showerror(io::IO, e::UnregisteredFunctionNameException) = print(io, "cppcall.UnregisteredFunctionNameException: no C++ function with name :" * string(e._function_name) * " registered") - - """ - an exception thrown when the number of arguments does not match the number of arguments - expected by the registered lambda - """ - mutable struct TupleSizeMismatchException <: Exception - - _function_name::Symbol - _expected::Int64 - _got::Int64 - end - Base.showerror(io::IO, e::TupleSizeMismatchException) = print(io, "cppcall.TupleSizeMismatchException: C++ function with name :" * string(e._function_name) * " expects " * string(e._expected) * " arguments but was called with " * string(e._got)) - - """ - `set_result(::Any) -> Nothing` - - modify _cppcall state result - """ - function set_result(x::Any) ::Nothing - - global _cppcall._state[]._result = x - return nothing - end - - """ - `get_result() -> Any` - - access _cppcall result - """ - function get_result() ::Any - - return _cppcall._state[]._result - end - - """ - `set_arguments(xs...) -> Nothing` - - modify _cppcall state argument tuple - """ - function set_arguments(xs...) ::Nothing - - global _cppcall._state[]._arguments = xs - return nothing - end - - """ - `get_result() -> Tuple` - - access _cppcall state argument tuple - """ - function get_arguments() ::Tuple - - return _cppcall._state[]._arguments - end - - """ - `verify_library() -> Bool` - - check if c_adapter library is available - """ - function verify_library() ::Bool - - if isfile(_cppcall._library_name) - return true - end - - message = "when trying to initialize jluna.cppcall: " - message *= "cannot find " * _cppcall._library_name - - println(sprint(Base.showerror, AssertionError(message), backtrace())) - return false - end - end - - # obfuscate internal state to encourage using operator[] sytanx - struct ProxyInternal - - _fieldnames_in_order::Vector{Symbol} - _fields::Dict{Symbol, Union{Any, Missing}} - ProxyInternal() = new(Vector{Symbol}(), Dict{Symbol, Union{Any, Missing}}()) - end - - # proxy as deepcopy of cpp-side usertype object - struct Proxy - - _typename::Symbol - _value::ProxyInternal - - 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 in template._value._fieldnames_in_order - push!(out.args[3].args, Expr(:(::), name, :($(typeof(template._value._fields[name]))))) - end - - new_call::Expr = Expr(:(=), Expr(:call, template._typename), Expr(:call, :new)) - - 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) - Base.eval(m, out) - return m.eval(template._typename) - end - export implement -end - -using Main.jluna; - -""" -`setindex!(::Proxy, <:Any, ::Symbol) -> Nothing` - -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 -export setindex! - -""" -`getindex(::Proxy, ::Symbol) -> Any` - -extend base.getindex -""" -function Base.getindex(proxy::Main.jluna.Proxy, value, key::Symbol) #::Auto - return proxy._value._fields[key] -end -export getindex - -""" -`cppall(::Symbol, ::Any...) -> Any` - -Call a lambda registered via `jluna::State::register_function` using `xs...` as arguments. -After the C++-side function returns, return the resulting object -(or `nothing` if the C++ function returns `void`) - -This function is not thread-safe and should not be used in a parallel context -""" -function cppcall(function_name::Symbol, xs...) ::Any - - id = hash(function_name) - - if !ccall((:is_registered, jluna._cppcall._library_name), Bool, (Csize_t,), id) - throw(jluna._cppcall.UnregisteredFunctionNameException(function_name)) - end - - n_args_expected = ccall((:get_n_args, jluna._cppcall._library_name), Csize_t, (Csize_t,), id) - if length(xs) != n_args_expected - throw(jluna._cppcall.TupleSizeMismatchException(function_name, n_args_expected, length(xs))) - end - - jluna._cppcall.set_arguments(xs...) - jluna._cppcall.set_result(nothing) - - ccall((:call_function, jluna._cppcall._library_name), Cvoid, (Csize_t,), id) - - return jluna._cppcall.get_result() -end -export cppcall \ No newline at end of file diff --git a/include/julia/01_common.jl b/include/julia/01_common.jl new file mode 100644 index 0000000..71e28c4 --- /dev/null +++ b/include/julia/01_common.jl @@ -0,0 +1,276 @@ +""" +`get_value_type_of_array(::Array{T}) -> Type` + +forward value type of array +""" +function get_value_type_of_array(_::Array{T}) ::Type where T + + return T +end + +""" +`get_reference_value(::Base.RefValue{T}) -> T` + +forward value of reference +""" +function get_reference_value(ref::Base.RefValue{T}) ::T where T + + return ref[]; +end + +setindex!(str::String, c::Char, i::Int64) = setindex!(collect(str), c, i) + +""" +`is_number_only(::String) -> Bool` + +check whether a string can be transformed into a base 10 number +""" +function is_number_only(x::String) ::Bool + + for s in x + if s != '0' || s != '1' || s != '2' || s != '3' || s != '4' || s != '5' || s != '6' || s != '7' || s != '8' || s != '9' + return false + end + end + + return true; +end + +""" +`is_method_available(::Function, ::Any) -> Bool` + +check if method of given function is available for a specific variable +""" +function is_method_available(f::Function, variable) ::Bool + + return hasmethod(f, Tuple{typeof(variable)}) +end + +""" +`get_function(::Symbol, ::Module) -> Function` + +exception-safe function access wrapper +""" +function get_function(function_name::Symbol, m::Module) ::Function + + return m.eval(function_name) +end +export get_function + +""" +`exists(<:AbstractArray, ::Any) -> Bool` + +check if element exists in array +""" +function exists(array::T, v::Any) ::Bool where T <: AbstractArray + + return !isempty(findall(x -> x == v, array)) +end + +""" +`tuple_at(::Tuple, ::Integer) -> Any` + +get nth element of tuple +""" +function tuple_at(x::Tuple, i::Integer) + return x[i] +end + +""" +`tuple_length(::Tuple) -> Integer` + +get length of tuple +""" +function tuple_length(x::Tuple{N}) where N + return N +end + +""" +`make_new(::String, xs...) -> Any` + +parse string to type, then call ctor with given args +""" +function make_new(name::String, xs...) ::Any + + return Main.eval(:($(Meta.parse(name))($(xs...)))) +end + +""" +`make_vector(::T...) -> Vector{T}` + +wrap vector ctor in varargs argument, used by box/unbox +""" +function make_vector(args::T...) ::Vector{T} where T + + return [args...] +end + +""" +`make_vector(t::Type) -> Vector{t}` + +create empty vector of given type +""" +function make_vector(t::Type) ::Vector{t} + + return Vector{t}() +end + +""" +`make_vector(::Type{T}, ::Any...) -> Vector{T}` + +create vector by converting all elements to target type +""" +function make_vector(type::Type{T}, xs...) where T + return convert.(T, [xs...]) +end + +""" +`new_vector(::Integer, ::T) -> Vector{T}` + +create vector by deducing argument type +""" +function new_vector(size::Integer, _::T) where T + return Vector{T}(undef, size) +end + +""" +`new_vector(::Integer, ::T) -> Vector{T}` + +create vector by deducing argument type +""" +function new_vector(size::Integer, type::Type) + return Vector{type}(undef, size) +end + +""" +`make_set(::T...) -> Set{T}` + +wrap set ctor in varargs argument, used by box/unbox +""" +function make_set(args::T...) ::Set{T} where T + + return Set([args...]); +end + +""" +`make_set(::Type{T}, ::Any...) -> Set{T}` + + +""" +function make_set(type::Type{T}, xs...) ::Set{T} where T + + return Set(make_vector(type, xs...)); +end + +""" +`make_pair(::T, ::U) -> Pair{T, U}` + +wrap pair ctor +""" +function make_pair(a::T, b::U) ::Pair{T, U} where {T, U} + + return a => b +end + +""" +`make_complex(:T, :T) -> Complex{T}` + +wrap complex ctor +""" +function make_complex(real::T, imag::T) ::Complex{T} where T + return Complex{T}(real, imag) +end + +""" +`assert_isa(::T, ::Symbol) -> Nothing` + +throw assertion if x is not of named type +""" +function assert_isa(x::Any, type::Type) ::Nothing + + if !(x isa type) + throw(AssertionError("expected " * string(type) * " but got an object of type " * string(typeof(x)))); + end + + return nothing +end + +""" +`convert(::T, symbol::Symbol) -> Any` + +convert value type, declared via symbol +""" +function convert(x::T, symbol::Symbol) ::Any where T + + type = Main.eval(symbol); + @assert type isa Type + + if T isa type + return T + end + + return Base.convert(type, x) +end + +""" +`string_to_type` -> Type + +evaluate string to type name if possible +""" +function string_to_type(str::String) ::Type + + res = Main.eval(Meta.parse(str)) + if !(res isa Type) + throw(UndefVarError(Symbol(str))) + end + return res +end + +""" +`invoke(function::Any, arguments::Any...) -> Any` + +wrap function call for non-function objects +""" +function invoke(x::Any, args...) ::Any + return x(args...) +end + +""" +`create_or_assign(::Symbol, ::T) -> T` + +assign variable in main, or if none exist, create it and assign +""" +function create_or_assign(symbol::Symbol, value::T) ::T where T + + return Main.eval(Expr(:(=), symbol, value)) +end + +""" +`serialize(<:AbstractDict{T, U}) -> Vector{Pair{T, U}}` + +transform dict into array +""" +function serialize(x::T) ::Vector{Pair{Key_t, Value_t}} where {Key_t, Value_t, T <: AbstractDict{Key_t, Value_t}} + + out = Vector{Pair{Key_t, Value_t}}() + for e in x + push!(out, e) + end + return out; +end + +""" +`serialize(::Set{T}) -> Vector{T}` + +transform dict into array +""" +function serialize(x::T) ::Vector{U} where {U, T <: AbstractSet{U}} + + out = Vector{U}() + + for e in x + push!(out, e) + end + + return out; +end \ No newline at end of file diff --git a/include/julia/02_common.jl b/include/julia/02_common.jl new file mode 100644 index 0000000..4faebd8 --- /dev/null +++ b/include/julia/02_common.jl @@ -0,0 +1,201 @@ +""" +`dot(::Array, field::Symbol) -> Any` + +wrapped dot operator +""" +function dot(x::Array, field_name::Symbol) ::Any + + index_maybe = parse(Int, string(field_name)); + @assert index_maybe isa Integer + return x[index_maybe]; +end +export dot; + +""" +`dot(::Module, field::Symbol) -> Any` + +wrapped dot operator +""" +dot(x::Module, field_name::Symbol) = return x.eval(field_name); + +""" +`dot(x::Any, field::Symbol) -> Any` + +wrapped dot operator, x.field +""" +dot(x::Any, field_name::Symbol) = return eval(:($x.$field_name)) + +""" +`unroll_type(::Type) -> Type` + +unroll type declaration +""" +function unroll_type(type::Type) ::Type + + while hasproperty(type, :body) + type = type.body + end + + return type +end + +""" +`is_name_typename(::Type, ::Type) -> Bool` + +unroll type declaration, then check if name is typename +""" +function is_name_typename(type_in::Type, type_comparison::Type) ::Bool + return getproperty(type_in, :name) == Base.typename(type_comparison) +end + +""" +`get_n_fields(::Type) -> Int64` +""" +function get_n_fields(type::Type) ::Int64 + return length(fieldnames(type)) +end + +""" +`get_fields(::Type) -> Vector{Pair{Symbol, Type}}` + +get field symbols and types, used by jluna::Type::get_fields +""" +function get_fields(type::Type) ::Vector{Pair{Symbol, Type}} + + out = Vector{Pair{Symbol, Type}}(); + names = fieldnames(type) + types = fieldtypes(type) + + for i in 1:(length(names)) + push!(out, names[i] => types[i]) + end + + return out +end + +""" +`get_parameter(::Type) -> Vector{Pair{Symbol, Type}}` + +get parameter symbols and upper type limits, used by jluna::Type::get_parameters +""" +function get_parameters(type::Type) ::Vector{Pair{Symbol, Type}} + + type = unroll_type(type) + + out = Vector{Pair{Symbol, Type}}(); + parameters = getproperty(type, :parameters) + + for i in 1:(length(parameters)) + push!(out, parameters[i].name => parameters[i].ub) + end + + return out +end + +""" +`get_n_parameters(::Type) -> Int64` +""" +function get_n_parameters(type::Type) ::Int64 + + type = unroll_type(type) + + return length(getproperty(type, :parameters)) +end + +""" +`assign_in_module(::Module, ::Symbol, ::T) -> T` + +assign variable in other module, throws if variable does not exist +""" +function assign_in_module(m::Module, variable_name::Symbol, value::T) ::T where T + + if (!isdefined(m, variable_name)) + throw(UndefVarError(Symbol(string(m) * "." * string(variable_name)))) + end + + return Base.eval(m, :($variable_name = $value)) +end + +""" +`create_in_module(::Module, ::Symbol, ::T) -> T` + +assign variable in other module, if variable does not exist, create then assign +""" +function create_or_assign_in_module(m::Module, variable_name::Symbol, value::T) ::T where T + return Base.eval(m, :($variable_name = $value)) +end + +""" +`get_names(::Module) -> IdDict{Symbol, Any}` + +access all module members as dict +""" +function get_names(m::Module) ::IdDict{Symbol, Any} + + out = IdDict{Symbol, Any}() + + for n in names(m; all = true) + if string(n)[1] != '#' + out[n] = m.eval(n) + end + end + + return out +end + +""" +`get_nth_method(::Function, ::Integer) -> Method` + +wrap method access, used by jlune::Method +""" +function get_nth_method(f::Function, i::Integer) ::Method + + return methods(f)[i] +end + +""" +`get_return_type_of_nth_method(::Function, ::Integer) -> Type` + +used by jluna::Function to deduce method signature +""" +function get_return_type_of_nth_method(f::Function, i::Integer) ::Type + + return Base.return_types(test)[i] +end + +""" +`get_argument_type_of_nths_methods(::Function, ::Integer) -> Vector{Type}` + +used by jluna::Function to deduce method signature +""" +function get_argument_types_of_nth_method(f::Function, i::Integer) ::Vector{Type} + + out = Vector{Type}() + types = methods(f)[i].sig.types + + for i in 2:length(types) + push!(out, types[i]) + end + + return out +end + + +""" +`get_length_of_generator(::Base.Generator) -> Int64` + +deduce length of Base.Generator object +""" +function get_length_of_generator(gen::Base.Generator) ::Int64 + + if (Base.haslength(gen)) + return length(gen) + else + # heuristically deduce length + for i in Iterators.reverse(gen.iter.itr) + if gen.iter.flt(i) + return i + end + end + end +end \ No newline at end of file diff --git a/include/julia/03_exception_handler.jl b/include/julia/03_exception_handler.jl new file mode 100644 index 0000000..d08601b --- /dev/null +++ b/include/julia/03_exception_handler.jl @@ -0,0 +1,133 @@ +""" +offers verbose exception interface. Any call with safe_call will store +the last exception and full stack trace as string in _last_exception and +_last_message respectively +""" +module exception_handler + + """ + exception Placeholder that signals that no exception occurred + """ + struct NoException <: Exception end + export NoException + + """ + current state of the exception handler + + _last_exception <:Exception + _last_message ::String + """ + mutable struct State + _last_exception + _last_message::String + _exception_occurred::Bool + end + + const _state = Ref{State}(State(NoException(), "", false)); + + """ + `safe_call(::Function, ::Any...) -> Any` + + call any function, update the handler then forward the result, if any + """ + function safe_call(f::Any, args...) + + result = undef + try + result = f(args...) + update() + catch exc + result = nothing + update(exc) + end + + return result; + end + + """ + `safe_call(::Expr, ::Module = Main) -> Any` + + call any line of code, update the handler then forward the result, if any + """ + function safe_call(expr::Expr, m::Module = Main) ::Any + + if expr.head == :block + expr.head = :toplevel + end + + return safe_call(Base.eval, m, expr); + end + + """ + `unsafe_call(::Expr, ::Module = Main) -> Any` + + call any line of code without updating the handler + """ + function unsafe_call(expr::Expr, m::Module = Main) ::Any + + if expr.head == :block + expr.head = :toplevel + end + + return Base.eval(m, expr); + end + + """ + `update(<:Exception) -> Nothing` + + update the handler after an exception was thrown + """ + function update(exception::Exception) ::Nothing + + try + global _state[]._last_message = sprint(Base.showerror, exception, catch_backtrace()) + global _state[]._last_exception = exception + global _state[]._exception_occurred = true + catch e end + return nothing + end + + """ + `update() -> Nothing` + + update the handler after *no* exception was thrown + """ + function update() ::Nothing + + if !(_state[]._last_exception isa NoException) + + global _state[]._last_message = "" + global _state[]._last_exception = NoException() + global _state[]._exception_occurred = false + end + return nothing + end + + """ + `has_exception_occurred() -> Bool` + + is last exception type no "jluna.exception_handler.NoException" + """ + function has_exception_occurred() ::Bool + + return _state[]._exception_occurred + end + + """ + `get_last_message() -> String` + + wrapper for _state access + """ + function get_last_message() ::String + return _state[]._last_message + end + + """ + `get_last_exception() -> Exception` + + wrapper for _state access + """ + function get_last_exception() ::Exception + return _state[]._last_exception + end +end \ No newline at end of file diff --git a/include/julia/04_memory_handler.jl b/include/julia/04_memory_handler.jl new file mode 100644 index 0000000..98aef55 --- /dev/null +++ b/include/julia/04_memory_handler.jl @@ -0,0 +1,177 @@ +""" +offers julia-side memory management for C++ jluna +""" +module memory_handler + + const _current_id = Ref(UInt64(0)); + const _refs = Ref(Dict{UInt64, Base.RefValue{Any}}()) + const _ref_counter = Ref(Dict{UInt64, UInt64}()) + + const _ref_id_marker = '#' + const _refs_expression = Meta.parse("jluna.memory_handler._refs[]") + + # proxy id that is actually an expression, the ID of topmodule Main is + ProxyID = Union{Expr, Symbol, Nothing} + + # make as unnamed + make_unnamed_proxy_id(id::UInt64) = return Expr(:ref, Expr(:ref, _refs_expression, id)) + + # make as named with owner and symbol name + make_named_proxy_id(id::Symbol, owner_id::ProxyID) ::ProxyID = return Expr(:(.), owner_id, QuoteNode(id)) + + # make as named with main as owner and symbol name + make_named_proxy_id(id::Symbol, owner_id::Nothing) ::ProxyID = return id + + # make as named with owner and array index name + make_named_proxy_id(id::Number, owner_id::ProxyID) ::ProxyID = return Expr(:ref, owner_id, convert(Int64, id)) + + # assign to proxy id + 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) + evaluate(name::Symbol) ::Any = return Main.eval(:($name)) + + """ + `get_name(::ProxyID) -> String` + + parse name from proxy id + """ + function get_name(id::ProxyID) ::String + + if length(id.args) == 0 + return "Main" + end + + current = id + while current.args[1] isa Expr && length(current.args) >= 2 + + if current.args[2] isa UInt64 + current.args[2] = convert(Int64, current.args[2]) + end + + current = current.args[1] + end + + out = string(id) + reg = r"\Q((jluna.memory_handler._refs[])[\E(.*)\Q])[]\E" + captures = match(reg, out) + + if captures != nothing + out = replace(out, reg => "") + end + + return out; + end + + get_name(::Nothing) ::String = return "Main" + get_name(s::Symbol) ::String = return string(s) + get_name(i::Integer) ::String = return "[" * string(i) * "]" + + """ + `print_refs() -> Nothing` + + pretty print _ref state, for debugging + """ + function print_refs() ::Nothing + + println("jluna.memory_handler._refs: "); + for e in _refs[] + println("\t", Int64(e.first), " => ", e.second[], " (", typeof(e.second[]), ") ") + end + end + + """ + `create_reference(::UInt64, ::Any) -> UInt64` + + add reference to _refs + """ + function create_reference(to_wrap::Any) ::UInt64 + + global _current_id[] += 1; + key::UInt64 = _current_id[]; + + if (haskey(_refs[], key)) + _ref_counter[][key] += 1 + else + _refs[][key] = Base.RefValue{Any}(to_wrap) + _ref_counter[][key] = 1 + end + + return key; + end + + create_reference(_::Nothing) ::UInt64 = return 0 + + """ + `set_reference(::UInt64, ::T) -> Nothing` + + update the value of a reference in _refs without adding a new entry or changing it's key, ref pointers C++ side stay valid + """ + function set_reference(key::UInt64, new_value::T) ::Base.RefValue{Any} where T + + _refs[][key] = Base.RefValue{Any}(new_value) + return _refs[][key] + end + + """ + `get_reference(::Int64) -> Any` + + access reference in _refs + """ + function get_reference(key::UInt64) ::Any + + if (key == 0) + return nothing + end + + return _refs[][key] + end + + """ + `free_reference(::UInt64) -> Nothing` + + free reference from _refs + """ + function free_reference(key::UInt64) ::Nothing + + if (key == 0) + return nothing; + end + + if _refs[][key][] isa Module + return + end + + global _ref_counter[][key] -= 1 + count::UInt64 = _ref_counter[][key] + + if (count == 0) + delete!(_ref_counter[], key) + delete!(_refs[], key) + end + + return nothing; + end + + """ + `force_free() -> Nothing` + + immediately deallocate all C++-managed values + """ + function force_free() ::Nothing + + for k in keys(_refs) + delete!(_refs[], k) + delete!(_ref_counter[], k) + end + + return nothing; + end +end \ No newline at end of file diff --git a/include/julia/05_cppcall.jl b/include/julia/05_cppcall.jl new file mode 100644 index 0000000..403efa1 --- /dev/null +++ b/include/julia/05_cppcall.jl @@ -0,0 +1,191 @@ +module _cppcall + + mutable struct State + _arguments::Tuple + _result::Any + + State() = new((), nothing) + end + + _library_name = "" + _state = Base.Ref{_cppcall.State}(State()) + + """ + Wrapper object for unnamed functions, frees function once object is destroyed + """ + mutable struct UnnamedFunctionProxy{N, Return_t} + + _id::Symbol + _call::Function + _n_args::Int64 + + function UnnamedFunctionProxy{N, Return_t}(id::Symbol) where {N, Return_t} + + @assert(-1 <= N <= 4) + + _id = id + x = new{N, Return_t}(id, function (xs...) Main.cppcall(_id, xs...) end, N) + + finalizer(function (t::UnnamedFunctionProxy{N, Return_t}) + ccall((:free_function, _cppcall._library_name), Cvoid, (Csize_t,), hash(t._id)) + end, x) + + return x + end + end + + # call with signature (1x Any) -> [Any / Nothing] + function (instance::UnnamedFunctionProxy{0, T})() ::T where T + return instance._call(); + end + + # call with signature (1x Any) -> [Any / Nothing] + function (instance::UnnamedFunctionProxy{1, T})(arg1) ::T where T + return instance._call(arg1); + end + + # call with signature (2x Any) -> [Any / Nothing] + function (instance::UnnamedFunctionProxy{2, T})(arg1, arg2) ::T where T + return instance._call(arg1, arg2); + end + + # call with signature (3x Any) -> [Any / Nothing] + function (instance::UnnamedFunctionProxy{3, T})(arg1, arg2, arg3) ::T where T + return instance._call(arg1, arg2, arg3); + end + + # call with signature (4x Any) -> [Any / Nothing] + function (instance::UnnamedFunctionProxy{4, T})(arg1, arg2, arg3, arg4) ::T where T + return instance._call(arg1, arg2, arg3, arg4); + end + + # call with other signature + function (instance::UnnamedFunctionProxy{Int64(-1), T})(args::Vector) ::T where T + return instance._call(args...); + end + + # ctor wrapper for jluna + new_unnamed_function(s::Symbol, N::Integer, T::Type) = return UnnamedFunctionProxy{N, T}(s) + + """ + an exception thrown when trying to invoke cppcall with a function name that + has not yet been registered via jluna::register_function + """ + mutable struct UnregisteredFunctionNameException <: Exception + + _function_name::Symbol + end + Base.showerror(io::IO, e::UnregisteredFunctionNameException) = print(io, "cppcall.UnregisteredFunctionNameException: no C++ function with name :" * string(e._function_name) * " registered") + + """ + an exception thrown when the number of arguments does not match the number of arguments + expected by the registered lambda + """ + mutable struct TupleSizeMismatchException <: Exception + + _function_name::Symbol + _expected::Int64 + _got::Int64 + end + Base.showerror(io::IO, e::TupleSizeMismatchException) = print(io, "cppcall.TupleSizeMismatchException: C++ function with name :" * string(e._function_name) * " expects " * string(e._expected) * " arguments but was called with " * string(e._got)) + + """ + `set_result(::Any) -> Nothing` + + modify _cppcall state result + """ + function set_result(x::Any) ::Nothing + + global _cppcall._state[]._result = x + return nothing + end + + """ + `get_result() -> Any` + + access _cppcall result + """ + function get_result() ::Any + + return _cppcall._state[]._result + end + + """ + `set_arguments(xs...) -> Nothing` + + modify _cppcall state argument tuple + """ + function set_arguments(xs...) ::Nothing + + global _cppcall._state[]._arguments = xs + return nothing + end + + """ + `get_result() -> Tuple` + + access _cppcall state argument tuple + """ + function get_arguments() ::Tuple + + return _cppcall._state[]._arguments + end + + """ + `verify_library() -> Bool` + + check if c_adapter library is available + """ + function verify_library() ::Bool + + if isfile(_cppcall._library_name) + return true + end + + message = "when trying to initialize jluna.cppcall: " + message *= "cannot find " * _cppcall._library_name + + println(sprint(Base.showerror, AssertionError(message), backtrace())) + return false + end +end + +# obfuscate internal state to encourage using operator[] sytanx +struct ProxyInternal + + _fieldnames_in_order::Vector{Symbol} + _fields::Dict{Symbol, Union{Any, Missing}} + ProxyInternal() = new(Vector{Symbol}(), Dict{Symbol, Union{Any, Missing}}()) +end + +# proxy as deepcopy of cpp-side usertype object +struct Proxy + + _typename::Symbol + _value::ProxyInternal + + 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 in template._value._fieldnames_in_order + push!(out.args[3].args, Expr(:(::), name, :($(typeof(template._value._fields[name]))))) + end + + new_call::Expr = Expr(:(=), Expr(:call, template._typename), Expr(:call, :new)) + + 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) + Base.eval(m, out) + return m.eval(template._typename) +end +export implement \ No newline at end of file diff --git a/include/julia/06_public.jl b/include/julia/06_public.jl new file mode 100644 index 0000000..7d03358 --- /dev/null +++ b/include/julia/06_public.jl @@ -0,0 +1,58 @@ +using Main.jluna; + +""" +`setindex!(::Proxy, <:Any, ::Symbol) -> Nothing` + +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 +export setindex! + +""" +`getindex(::Proxy, ::Symbol) -> Any` + +extend base.getindex +""" +function Base.getindex(proxy::Main.jluna.Proxy, value, key::Symbol) #::Auto + return proxy._value._fields[key] +end +export getindex + +""" +`cppall(::Symbol, ::Any...) -> Any` + +Call a lambda registered via `jluna::State::register_function` using `xs...` as arguments. +After the C++-side function returns, return the resulting object +(or `nothing` if the C++ function returns `void`) + +This function is not thread-safe and should not be used in a parallel context +""" +function cppcall(function_name::Symbol, xs...) ::Any + + id = hash(function_name) + + if !ccall((:is_registered, jluna._cppcall._library_name), Bool, (Csize_t,), id) + throw(jluna._cppcall.UnregisteredFunctionNameException(function_name)) + end + + n_args_expected = ccall((:get_n_args, jluna._cppcall._library_name), Csize_t, (Csize_t,), id) + if length(xs) != n_args_expected + throw(jluna._cppcall.TupleSizeMismatchException(function_name, n_args_expected, length(xs))) + end + + jluna._cppcall.set_arguments(xs...) + jluna._cppcall.set_result(nothing) + + ccall((:call_function, jluna._cppcall._library_name), Cvoid, (Csize_t,), id) + + return jluna._cppcall.get_result() +end +export cppcall \ No newline at end of file diff --git a/include/julia_extension.hpp b/include/julia_extension.hpp index 67acfcd..d1720c9 100644 --- a/include/julia_extension.hpp +++ b/include/julia_extension.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include diff --git a/include/julia_wrapper.hpp b/include/julia_wrapper.hpp new file mode 100644 index 0000000..4f2acf1 --- /dev/null +++ b/include/julia_wrapper.hpp @@ -0,0 +1,12 @@ +#pragma once + +#ifdef _WIN32 +// julia.h includes Windows headers that are not self contained. This wrapper +// header includes Windows.h before including julia.h, so everything is setup +// for the other headers to actually work. +# define NOMINMAX +# define WIN32_LEAN_AND_MEAN +# include +#endif + +#include diff --git a/include/module.hpp b/include/module.hpp index d686560..cad9020 100644 --- a/include/module.hpp +++ b/include/module.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -121,4 +121,4 @@ namespace jluna }; } -#include ".src/module.inl" \ No newline at end of file +#include <.src/module.inl> \ No newline at end of file diff --git a/include/proxy.hpp b/include/proxy.hpp index d37e938..ee281fb 100644 --- a/include/proxy.hpp +++ b/include/proxy.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -207,4 +207,4 @@ namespace jluna }; } -#include ".src/proxy.inl" \ No newline at end of file +#include <.src/proxy.inl> \ No newline at end of file diff --git a/include/state.hpp b/include/state.hpp index 6281e1e..16e3182 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -28,6 +28,9 @@ namespace jluna namespace jluna::State { + /// @brief manually set the C-adapter path + void set_c_adapter_path(const std::string& path); + /// @brief initialize environment void initialize(); @@ -406,4 +409,4 @@ namespace jluna::State [[deprecated("use State::safe_eval instead")]] Proxy safe_script(const std::string&); } -#include ".src/state.inl" \ No newline at end of file +#include <.src/state.inl> \ No newline at end of file diff --git a/include/symbol.hpp b/include/symbol.hpp index a394ef1..d7800b7 100644 --- a/include/symbol.hpp +++ b/include/symbol.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include namespace jluna @@ -68,4 +68,4 @@ namespace jluna }; } -#include ".src/symbol.inl" \ No newline at end of file +#include <.src/symbol.inl> \ No newline at end of file diff --git a/include/type.hpp b/include/type.hpp index 628209b..91e39bc 100644 --- a/include/type.hpp +++ b/include/type.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include #include @@ -206,4 +206,4 @@ namespace jluna inline Type WeakRef_t; } -#include ".src/type.inl" \ No newline at end of file +#include <.src/type.inl> \ No newline at end of file diff --git a/include/typedefs.hpp b/include/typedefs.hpp index 2b1382c..7f73aa2 100644 --- a/include/typedefs.hpp +++ b/include/typedefs.hpp @@ -5,8 +5,8 @@ #pragma once -#include -#include ".src/typedefs.inl" +#include +#include <.src/typedefs.inl> namespace jluna { diff --git a/include/unbox.hpp b/include/unbox.hpp index beccc84..605f238 100644 --- a/include/unbox.hpp +++ b/include/unbox.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -154,4 +154,4 @@ namespace jluna }; } -#include ".src/unbox.inl" \ No newline at end of file +#include <.src/unbox.inl> \ No newline at end of file diff --git a/include/usertype.hpp b/include/usertype.hpp index 9d34edd..f0f68a0 100644 --- a/include/usertype.hpp +++ b/include/usertype.hpp @@ -5,7 +5,7 @@ #pragma once -#include +#include #include #include @@ -86,4 +86,4 @@ namespace jluna }; } -#include ".src/usertype.inl" \ No newline at end of file +#include <.src/usertype.inl> \ No newline at end of file diff --git a/install/init.sh b/install/init.sh new file mode 100644 index 0000000..4c75d4d --- /dev/null +++ b/install/init.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# init.sh +# +# this bash script creates a new C++ project using jluna. +# It is intended for novice users, advanced users are encouraged to +# `make install` jluna and link it and julia to their own project manually +# +# for more information, visit https://github.com/Clemapfel/jluna/blob/master/docs/installation.md +# +# @param $1: project name (i.e. "MyProject") +# @param $2: project root path (i.e "/home/user/Desktop") +# @param $3: (optional) compiler. One of: "g++-10", "g++-11", "clang++-12" +# @param $4: (optional) If `TRUE`, overwrites folder if it already exists. +# + +# catch input +if [ -z "$1" ] | [ -z "$2" ]; then + echo "Usage: /bin/bash init.sh [ = clang++-12]" + exit 1 +fi + +# default compiler +if [ -n "$3" ]; then + compiler="clang++-12" +else + compiler="$3" +fi + +# catch unset JULIA_BINDIR +if [ -z "$JULIA_BINDIR" ]; then + printf "JULIA_BINDIR was not set. Please do so manually, using \`export JULIA_BINDIR=\$(julia -e \"println(Sys.BINDIR)\")\`\n" + exit 0 +fi + +# project directory +project_name="$1" +project_root="$2" + +# prevent accidental deletion +if [ -d "$project_root/$project_name" ]; then + + if [ "$4" == "TRUE" ]; then + rm -r "$project_root/$project_name" + else + printf "Folder $project_root/$project_name already exists. Please first delete it manually or specify a different directory\n" + exit 0 + fi +fi + +# setup project folder +echo "creating $project_root/$project_name..." + +cd $project_root +mkdir -p $project_name +cd $project_name + +# clone jluna +git clone https://github.com/Clemapfel/jluna.git + +# build jluna +cd jluna +mkdir -p build +cd build + +cmake .. -DCMAKE_CXX_COMPILER=$compiler -DCMAKE_INSTALL_PREFIX="$project_root/$project_name/jluna/build" +make + +# copy main.cpp, CMakeLists.txt and FindJulia into project folder +cd ../.. +cp ./jluna/install/resources/CMakeLists.txt ./CMakeLists.txt +sed -i "s/@PROJECT_NAME@/$project_name/" ./CMakeLists.txt + +cp ./jluna/install/resources/main.cpp ./main.cpp + +mkdir -p $project_root/$project_name/cmake/find +cp ./jluna/install/resources/FindJulia.cmake ./cmake/find/FindJulia.cmake + +# build project +cd $project_root/$project_name +mkdir build +cd build +cmake .. -DCMAKE_CXX_COMPILER=$compiler +make + +# execute hello world +printf "\n" +./hello_world + +# exit +exit 0 diff --git a/install/resources/CMakeLists.txt b/install/resources/CMakeLists.txt new file mode 100644 index 0000000..f68b163 --- /dev/null +++ b/install/resources/CMakeLists.txt @@ -0,0 +1,32 @@ +# this file was generate by jluna/install/init.sh +cmake_minimum_required(VERSION 3.12) +project(@PROJECT_NAME@ VERSION 0.0.0 LANGUAGES CXX) + +# find julia +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/find") +find_package(Julia 1.7.0 REQUIRED) + +# find jluna +set(CMAKE_FIND_LIBRARY_SUFFIXES_WIN32 ".lib;.dll;.dll.a;") +set(CMAKE_FIND_LIBRARY_SUFFIXES_UNIX ".so") + +find_library(jluna REQUIRED + NAMES jluna + PATHS "${CMAKE_SOURCE_DIR}/jluna/build") + +# executable +add_executable(hello_world "${CMAKE_SOURCE_DIR}/main.cpp") +target_link_libraries(hello_world PRIVATE + "${jluna}" + "$" +) + +# set C++ standard +target_compile_features(hello_world PRIVATE cxx_std_20) + +# include directories +target_include_directories(hello_world PRIVATE + "${CMAKE_SOURCE_DIR}" + "${CMAKE_SOURCE_DIR}/jluna" + "${JULIA_INCLUDE_DIR}" +) \ No newline at end of file diff --git a/install/resources/FindJulia.cmake b/install/resources/FindJulia.cmake new file mode 100644 index 0000000..3c7352a --- /dev/null +++ b/install/resources/FindJulia.cmake @@ -0,0 +1,131 @@ +# +# This FindJulia.cmake is intended to detect julia as a package. +# +# Use +# `list(APPEND CMAKE_MODULE_PATH "path/to/this/file")` +# `find_package(Julia 1.7.0 REQUIRED)` +# +# to make the julia target available through: +# `"$"` +# +# Furthermore the following variables will be set on +# successful detection: +# +# JULIA_LIBRARY : julia shared library +# JULIA_EXECUTABLE : julia REPL executable +# JULIA_BINDIR : directory to julia binary +# JULIA_INCLUDE_DIR : directory that contains julia.h +# + +macro(julia_bail_if_false message var) + if(NOT ${var}) + set(Julia_FOUND 0) + set(Julia_NOT_FOUND_MESSAGE "${message}") + return() + endif() +endmacro() + +# detect julia executable +find_program(JULIA_EXECUTABLE julia PATHS ENV JULIA_BINDIR) +julia_bail_if_false("Unable to detect the julia executable. Make sure JULIA_BINDIR is set correctly." JULIA_EXECUTABLE) + +# detect julia binary dir +if(NOT DEFINED JULIA_BINDIR) + # The executable could be a chocolatey shim, so run some Julia code to report + # the path of the BINDIR + execute_process( + COMMAND "${JULIA_EXECUTABLE}" -e "print(Sys.BINDIR)" + OUTPUT_VARIABLE JULIA_BINDIR_LOCAL + ) + file(TO_CMAKE_PATH "${JULIA_BINDIR_LOCAL}" JULIA_BINDIR_LOCAL) + set(JULIA_BINDIR "${JULIA_BINDIR_LOCAL}" CACHE PATH "") +endif() +get_filename_component(JULIA_PATH_PREFIX "${JULIA_BINDIR}" DIRECTORY) + +if(WIN32) + set(julia_old_CMAKE_FIND_LIBRARY_SUFFIXES "") + set(julia_old_CMAKE_FIND_LIBRARY_PREFIXES "") + if(CMAKE_FIND_LIBRARY_SUFFIXES) + set(julia_old_CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_FIND_LIBRARY_SUFFIXES}") + list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES .dll.a) + else() + set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib;.dll.a") + endif() + if(CMAKE_FIND_LIBRARY_PREFIXES) + set(julia_old_CMAKE_FIND_LIBRARY_PREFIXES "${CMAKE_FIND_LIBRARY_PREFIXES}") + list(APPEND CMAKE_FIND_LIBRARY_PREFIXES lib) + else() + set(CMAKE_FIND_LIBRARY_PREFIXES ";lib") + endif() +endif() + +# detect julia library +find_library(JULIA_LIBRARY julia HINTS "${JULIA_PATH_PREFIX}/lib") + +if(WIN32) + set(CMAKE_FIND_LIBRARY_SUFFIXES "${julia_old_CMAKE_FIND_LIBRARY_SUFFIXES}") + set(CMAKE_FIND_LIBRARY_PREFIXES "${julia_old_CMAKE_FIND_LIBRARY_PREFIXES}") +endif() + +julia_bail_if_false("Unable to find the julia shared library. Make sure JULIA_BINDIR is set correctly and that the julia image is uncompressed" JULIA_LIBRARY) + +# detect julia include dir +find_path( + JULIA_INCLUDE_DIR julia.h + HINTS "${JULIA_PATH_PREFIX}/include" "${JULIA_PATH_PREFIX}/include/julia" +) +julia_bail_if_false("Unable to find julia.h. Make sure JULIA_BINDIR is set correctly and that your image is uncompressed." JULIA_INCLUDE_DIR) + +# detect julia version +if(NOT DEFINED JULIA_VERSION) + file(STRINGS "${JULIA_INCLUDE_DIR}/julia_version.h" JULIA_VERSION_LOCAL LIMIT_COUNT 1 REGEX JULIA_VERSION_STRING) + string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" JULIA_VERSION_LOCAL "${JULIA_VERSION_LOCAL}") + set(JULIA_VERSION "${JULIA_VERSION_LOCAL}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Julia + REQUIRED_VARS JULIA_LIBRARY JULIA_EXECUTABLE JULIA_BINDIR JULIA_INCLUDE_DIR + VERSION_VAR JULIA_VERSION +) + +# detect target properties +if(NOT TARGET Julia::Julia) + set(julia_has_implib NO) + set(julia_library_type STATIC) + if(JULIA_LIBRARY MATCHES "\\.(so|dylib)$") + set(julia_library_type SHARED) + elseif(JULIA_LIBRARY MATCHES "\\.(lib|dll\\.a)$") + set(julia_library_type UNKNOWN) + find_file( + JULIA_LIBRARY_DLL + NAMES libjulia.dll julia.dll + HINTS "${JULIA_BINDIR}" + ) + if(JULIA_LIBRARY_DLL) + set(julia_has_implib YES) + set(julia_library_type SHARED) + endif() + endif() + + add_library(Julia::Julia "${julia_library_type}" IMPORTED) + set_target_properties( + Julia::Julia PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${JULIA_INCLUDE_DIR}" + IMPORTED_LINK_INTERFACE_LANGUAGES C + ) + if(julia_has_implib) + if(JULIA_LIBRARY AND EXISTS "${JULIA_LIBRARY}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_IMPLIB "${JULIA_LIBRARY}") + endif() + if(JULIA_LIBRARY_DLL AND EXISTS "${JULIA_LIBRARY_DLL}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_LOCATION "${JULIA_LIBRARY_DLL}") + endif() + elseif(JULIA_LIBRARY AND EXISTS "${JULIA_LIBRARY}") + set_property(TARGET Julia::Julia PROPERTY IMPORTED_LOCATION "${JULIA_LIBRARY}") + endif() +endif() + +# finish +mark_as_advanced(JULIA_EXECUTABLE JULIA_BINDIR JULIA_LIBRARY JULIA_INCLUDE_DIR JULIA_VERSION JULIA_LIBRARY_DLL) diff --git a/install/resources/main.cpp b/install/resources/main.cpp new file mode 100644 index 0000000..4b7c0e0 --- /dev/null +++ b/install/resources/main.cpp @@ -0,0 +1,11 @@ +// this file was generated by jluna/init.sh + +#include + +using namespace jluna; + +int main() +{ + State::initialize(); + Base["println"]("Your project is setup and working!"); +}