├── .gitignore ├── cpp ├── .gitignore ├── CMakeLists.txt ├── test │ └── java.cpp └── include │ └── ktbind │ └── ktbind.hpp ├── kotlin ├── .gitignore ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle.kts ├── gradlew.bat ├── gradlew └── src │ └── test │ └── kotlin │ └── com │ └── kheiron │ └── ktbind │ └── NativeBindingsTest.kt ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | -------------------------------------------------------------------------------- /cpp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /kotlin/.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /build 4 | -------------------------------------------------------------------------------- /kotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.warning.mode=all 3 | -------------------------------------------------------------------------------- /kotlin/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kheiron-Medical/ktbind/HEAD/kotlin/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # set the project name 4 | project(ktbind LANGUAGES CXX) 5 | 6 | # Java integration 7 | find_package(JNI REQUIRED) 8 | 9 | # compiler configuration 10 | set(CMAKE_CXX_STANDARD 17) 11 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 12 | set(CMAKE_CXX_FLAGS -Wfatal-errors) 13 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 14 | 15 | # sources 16 | set(header_files ${CMAKE_CURRENT_SOURCE_DIR}/include/ktbind/ktbind.hpp) 17 | 18 | # header-only target for Java/Kotlin integration 19 | add_library(ktbind INTERFACE) 20 | target_sources(ktbind INTERFACE "$") 21 | target_include_directories(ktbind INTERFACE $) 22 | 23 | # shared library for unit tests 24 | add_library(ktbind_java MODULE test/java.cpp) 25 | add_dependencies(ktbind_java ktbind) 26 | target_include_directories(ktbind_java PRIVATE ${JNI_INCLUDE_DIRS}) 27 | target_link_libraries(ktbind_java PRIVATE ktbind ${JAVA_JVM_LIBRARY}) 28 | 29 | # installer 30 | install(DIRECTORY include/ktbind DESTINATION include) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kheiron Medical Technologies Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /kotlin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("application") 4 | id("org.jetbrains.kotlin.jvm") version "1.4.0" 5 | } 6 | 7 | group = "com.kheiron.ktbind" 8 | version = "1.0.0" 9 | 10 | apply(plugin = "java") 11 | apply(plugin = "application") 12 | apply(plugin = "org.jetbrains.kotlin.jvm") 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | // Kotlin standard library 20 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 21 | 22 | // Logging 23 | implementation("org.apache.logging.log4j:log4j-api:2.13.0") 24 | implementation("org.apache.logging.log4j:log4j-core:2.13.0") 25 | runtimeOnly("org.apache.logging.log4j:log4j-slf4j18-impl:2.13.0") 26 | 27 | // Testing 28 | testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") 29 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0") 30 | testImplementation("io.mockk:mockk:1.10.0+") 31 | } 32 | 33 | val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks 34 | val compileTestKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks 35 | 36 | compileKotlin.kotlinOptions { 37 | jvmTarget = "11" 38 | } 39 | compileTestKotlin.kotlinOptions { 40 | jvmTarget = "11" 41 | } 42 | java { 43 | sourceCompatibility = JavaVersion.VERSION_11 44 | targetCompatibility = JavaVersion.VERSION_11 45 | } 46 | 47 | tasks.named("test") { 48 | useJUnitPlatform { 49 | systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class") 50 | } 51 | testLogging { 52 | events("passed", "skipped", "failed") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /kotlin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /kotlin/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /cpp/test/java.cpp: -------------------------------------------------------------------------------- 1 | #include "ktbind/ktbind.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | template 7 | std::ostream& write_bracketed_list(std::ostream& os, const L& vec, char left, char right) { 8 | os << left; 9 | if (!vec.empty()) { 10 | auto&& it = vec.begin(); 11 | os << *it; 12 | 13 | for (++it; it != vec.end(); ++it) { 14 | os << ", " << *it; 15 | } 16 | } 17 | os << right; 18 | return os; 19 | } 20 | 21 | template 22 | std::ostream& write_list(std::ostream& os, const L& vec) { 23 | return write_bracketed_list(os, vec, '[', ']'); 24 | } 25 | 26 | template 27 | std::ostream& operator<<(std::ostream& os, const std::vector& list) { 28 | return write_list(os, list); 29 | } 30 | 31 | template 32 | std::ostream& operator<<(std::ostream& os, const std::list& list) { 33 | return write_list(os, list); 34 | } 35 | 36 | template 37 | std::ostream& write_set(std::ostream& os, const L& vec) { 38 | return write_bracketed_list(os, vec, '{', '}'); 39 | } 40 | 41 | template 42 | std::ostream& operator<<(std::ostream& os, const std::unordered_set& set) { 43 | return write_set(os, set); 44 | } 45 | 46 | template 47 | std::ostream& operator<<(std::ostream& os, const std::set& set) { 48 | return write_set(os, set); 49 | } 50 | 51 | template 52 | std::ostream& write_map(std::ostream& os, const M& map) { 53 | os << "{"; 54 | if (!map.empty()) { 55 | auto&& it = map.begin(); 56 | os << it->first << ": " << it->second; 57 | 58 | for (++it; it != map.end(); ++it) { 59 | os << ", " << it->first << ": " << it->second; 60 | } 61 | } 62 | os << "}"; 63 | return os; 64 | } 65 | 66 | template 67 | std::ostream& operator<<(std::ostream& os, const std::map& map) { 68 | return write_map(os, map); 69 | } 70 | 71 | template 72 | std::ostream& operator<<(std::ostream& os, const std::unordered_map& map) { 73 | return write_map(os, map); 74 | } 75 | 76 | struct Data { 77 | bool b = true; 78 | short s = 82; 79 | int i = 1024; 80 | long l = 111000111000; 81 | float f = M_PI; 82 | double d = M_E; 83 | std::string str = "sample string in struct"; 84 | std::vector short_arr = { 1,2,3,4,5,6,7,8,9 }; 85 | std::vector int_arr = { 10,20,30,40,50,60,70,80,90 }; 86 | std::vector long_arr = { 100,200,300,400,500,600,700,800,900 }; 87 | std::map> map = { 88 | {"one", {"a","b","c"}}, 89 | {"two", {"x","y","z"}}, 90 | {"three", {}}, 91 | {"four", {"l"}} 92 | }; 93 | }; 94 | 95 | std::ostream& operator<<(std::ostream& str, const Data& data) { 96 | return str << "{" 97 | << "b=" << data.b << ", " 98 | << "s=" << data.s << ", " 99 | << "i=" << data.i << ", " 100 | << "l=" << data.l << ", " 101 | << "f=" << data.f << ", " 102 | << "d=" << data.d << ", " 103 | << "str='" << data.str << "'" 104 | << "}"; 105 | } 106 | 107 | struct Sample { 108 | Sample(); 109 | Sample(const char*); 110 | Sample(std::string); 111 | Sample(Sample&&) = default; 112 | ~Sample(); 113 | Sample duplicate() const; 114 | Data get_data() const; 115 | void set_data(); 116 | void set_data(const Data& data); 117 | 118 | private: 119 | Sample(const Sample&) = default; // ensure that external code cannot make copies 120 | 121 | Data _data; 122 | }; 123 | 124 | Sample::Sample() { 125 | JAVA_OUTPUT << "created" << std::endl; 126 | } 127 | 128 | Sample::Sample(const char*) { 129 | JAVA_OUTPUT << "created from const char*" << std::endl; 130 | } 131 | 132 | Sample::Sample(std::string) { 133 | JAVA_OUTPUT << "created from string" << std::endl; 134 | } 135 | 136 | Sample::~Sample() { 137 | JAVA_OUTPUT << "destroyed" << std::endl; 138 | } 139 | 140 | Sample Sample::duplicate() const { 141 | JAVA_OUTPUT << "duplicated" << std::endl; 142 | return Sample(*this); 143 | } 144 | 145 | Data Sample::get_data() const { 146 | JAVA_OUTPUT << "get nested data" << std::endl; 147 | return _data; 148 | } 149 | 150 | void Sample::set_data() { 151 | _data = Data(); 152 | JAVA_OUTPUT << "set nested data: " << _data << std::endl; 153 | } 154 | 155 | void Sample::set_data(const Data& data) { 156 | _data = data; 157 | JAVA_OUTPUT << "set nested data: " << _data << std::endl; 158 | } 159 | 160 | void returns_void() {} 161 | 162 | bool returns_bool() { 163 | return true; 164 | } 165 | 166 | template 167 | T returns_integer() { 168 | return std::numeric_limits::max(); 169 | } 170 | 171 | float returns_float() { 172 | return std::numeric_limits::max(); 173 | } 174 | 175 | double returns_double() { 176 | return std::numeric_limits::max(); 177 | } 178 | 179 | std::string returns_string() { 180 | return "a sample string"; 181 | } 182 | 183 | bool pass_arguments_by_value(std::string str, bool b, short s, int i, long l, int16_t i16, int32_t i32, int64_t i64, float f, double d) { 184 | JAVA_OUTPUT << "(" 185 | << "string = " << str << ", " 186 | << "bool = " << b << ", " 187 | << "short = " << s << ", " 188 | << "int = " << i << ", " 189 | << "long = " << l << ", " 190 | << "int16_t = " << i16 << ", " 191 | << "int32_t = " << i32 << ", " 192 | << "int64_t = " << i64 << ", " 193 | << "float = " << f << ", " 194 | << "double = " << d << ")" 195 | << std::endl; 196 | return true; 197 | } 198 | 199 | bool pass_arguments_by_reference(const std::string& str, const bool& b, const short& s, const int& i, const long& l, const int16_t& i16, const int32_t& i32, const int64_t& i64, const float& f, const double& d) { 200 | JAVA_OUTPUT << "(" 201 | << "string = " << str << ", " 202 | << "bool = " << b << ", " 203 | << "short = " << s << ", " 204 | << "int = " << i << ", " 205 | << "long = " << l << ", " 206 | << "int16_t = " << i16 << ", " 207 | << "int32_t = " << i32 << ", " 208 | << "int64_t = " << i64 << ", " 209 | << "float = " << f << ", " 210 | << "double = " << d << ")" 211 | << std::endl; 212 | return true; 213 | } 214 | 215 | std::vector array_of_char(const std::vector& vec) { 216 | JAVA_OUTPUT << vec << std::endl; 217 | return { 'a', 'b', 'c', 'd', 'e', 'f' }; 218 | } 219 | 220 | std::vector array_of_int(const std::vector& vec) { 221 | JAVA_OUTPUT << vec << std::endl; 222 | return { 0, 1, 2, 3, 4, 5, 6 }; 223 | } 224 | 225 | std::vector array_of_string(const std::vector& vec) { 226 | JAVA_OUTPUT << vec << std::endl; 227 | return { "", "A", "B", "C", "D", "E", "F" }; 228 | } 229 | 230 | std::list list_of_int(const std::list& vec) { 231 | JAVA_OUTPUT << vec << std::endl; 232 | return { 1, 2, 3, 4, 5, 6 }; 233 | } 234 | 235 | std::list list_of_string(const std::list& vec) { 236 | JAVA_OUTPUT << vec << std::endl; 237 | return { "A", "B", "C", "D", "E", "F" }; 238 | } 239 | 240 | std::unordered_set unordered_set(std::unordered_set s) { 241 | JAVA_OUTPUT << s << std::endl; 242 | return { "A", "B", "C" }; 243 | } 244 | 245 | std::set ordered_set(std::set s) { 246 | JAVA_OUTPUT << s << std::endl; 247 | return { "A", "B", "C" }; 248 | } 249 | 250 | std::unordered_map unordered_map(std::unordered_map m) { 251 | JAVA_OUTPUT << m << std::endl; 252 | return { {"1", "A"}, {"2", "B"}, {"3", "C"} }; 253 | } 254 | 255 | std::map ordered_map_of_int(std::map m) { 256 | JAVA_OUTPUT << m << std::endl; 257 | return { {1, 1000}, {2, 2000}, {3, 3000} }; 258 | } 259 | 260 | std::map ordered_map_of_string(std::map m) { 261 | JAVA_OUTPUT << m << std::endl; 262 | return { {"1", "A"}, {"2", "B"}, {"3", "C"} }; 263 | } 264 | 265 | std::map> native_composite(std::map> m) { 266 | JAVA_OUTPUT << m << std::endl; 267 | return { { "A", {"a", "b", "c"} }, { "B", {} }, { "C", {"x"} } }; 268 | } 269 | 270 | void pass_callback(std::function fun) { 271 | fun(); 272 | } 273 | 274 | std::string pass_callback_returns_string(std::function fun) { 275 | return fun(); 276 | } 277 | 278 | int pass_callback_string_returns_int(std::string str, std::function fun) { 279 | return fun(str); 280 | } 281 | 282 | std::string pass_callback_string_returns_string(std::string str, std::function fun) { 283 | return fun(str); 284 | } 285 | 286 | std::string pass_callback_arguments(std::string str, std::function fun) { 287 | return fun(str, 4, 82, 112); 288 | } 289 | 290 | void callback_on_native_thread(std::function fun) { 291 | std::thread([fun]() { 292 | fun(); 293 | }).join(); 294 | } 295 | 296 | void raise_native_exception() { 297 | throw std::runtime_error("an expected error"); 298 | } 299 | 300 | void catch_java_exception(std::function fun) { 301 | try { 302 | fun(); 303 | } catch (std::exception& ex) { 304 | JAVA_OUTPUT << "exception caught: " << ex.what() << std::endl; 305 | } 306 | } 307 | 308 | DECLARE_DATA_CLASS(Data, "com.kheiron.ktbind.Data") 309 | DECLARE_NATIVE_CLASS(Sample, "com.kheiron.ktbind.Sample") 310 | 311 | JAVA_EXTENSION_MODULE() { 312 | using namespace java; 313 | 314 | native_class() 315 | .constructor("create") 316 | .constructor("create") 317 | .function<&Sample::duplicate>("duplicate") 318 | .function<&Sample::get_data>("get_data") 319 | .function(&Sample::set_data)>("set_data") 320 | 321 | // fundamental types and simple well-known types as return values 322 | .function("returns_void") 323 | .function("returns_bool") 324 | .function>("returns_short") 325 | .function>("returns_int") 326 | .function>("returns_long") 327 | .function>("returns_int16") 328 | .function>("returns_int32") 329 | .function>("returns_int64") 330 | .function("returns_float") 331 | .function("returns_double") 332 | .function("returns_string") 333 | 334 | // passing parameters by value and reference 335 | .function("pass_arguments_by_value") 336 | .function("pass_arguments_by_reference") 337 | 338 | // collections 339 | .function("array_of_char") 340 | .function("array_of_int") 341 | .function("array_of_string") 342 | .function("list_of_int") 343 | .function("list_of_string") 344 | .function("unordered_set") 345 | .function("ordered_set") 346 | .function("unordered_map") 347 | .function("ordered_map_of_int") 348 | .function("ordered_map_of_string") 349 | .function("native_composite") 350 | 351 | // callbacks 352 | .function("pass_callback") 353 | .function("pass_callback_returns_string") 354 | .function("pass_callback_string_returns_int") 355 | .function("pass_callback_string_returns_string") 356 | .function("pass_callback_arguments") 357 | .function("callback_on_native_thread") 358 | 359 | // exception handling 360 | .function("raise_native_exception") 361 | .function("catch_java_exception") 362 | ; 363 | 364 | data_class() 365 | .field<&Data::b>("b") 366 | .field<&Data::s>("s") 367 | .field<&Data::i>("i") 368 | .field<&Data::l>("l") 369 | .field<&Data::f>("f") 370 | .field<&Data::d>("d") 371 | .field<&Data::str>("str") 372 | .field<&Data::short_arr>("short_arr") 373 | .field<&Data::int_arr>("int_arr") 374 | .field<&Data::long_arr>("long_arr") 375 | .field<&Data::map>("map") 376 | ; 377 | 378 | print_registered_bindings(); 379 | } 380 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KtBind: C++ and Kotlin interoperability 2 | 3 | KtBind is a lightweight C++17 header-only library that exposes C++ types to Kotlin and vice versa, mainly to create Kotlin bindings for existing C++ code. The objective of KtBind is to provide an easy-to-use interoperability interface that feels natural from both C++ and Kotlin. KtBind uses C++ compile-time introspection to generate Java Native Interface (JNI) stubs that can be accessed from Kotlin with regular function invocation. The stubs incur minimal or no overhead compared to hand-written JNI code. 4 | 5 | This project has been inspired by a similar binding interface between JavaScript and C++ in [emscripten](https://emscripten.org), and between Python and C++ in [PyBind11](https://pybind11.readthedocs.io/en/stable/) and [Boost.Python](https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/index.html). Unlike [JNA](https://github.com/java-native-access/jna), which one can utilize by means of an intermediary C interface, KtBind offers a direct interface between C++ and Kotlin. 6 | 7 | ## Core features 8 | 9 | The following core C++ features can be mapped to Kotlin: 10 | 11 | * Functions accepting and returning custom data structures by value or by const reference 12 | * Instance methods and static methods 13 | * Overloaded functions 14 | * Operators 15 | * Instance attributes and static attributes 16 | * Arbitrary exception types 17 | * Callbacks and function objects 18 | * STL data structures 19 | 20 | Furthermore, the following core Kotlin features are exposed seamlessly to C++: 21 | 22 | * Collection types 23 | * Higher-order functions and lambda expressions 24 | * Arbitrary exception types 25 | 26 | ## Getting started 27 | 28 | KtBind is a header-only library, including the interoperability header in your C++ project allows you to create bindings to Kotlin: 29 | ```cpp 30 | #include 31 | ``` 32 | 33 | Consider the following C++ class as an example: 34 | ```cpp 35 | struct Sample { 36 | Sample(); 37 | Sample(const char*); 38 | Sample(std::string); 39 | Data get_data(); 40 | void set_data(const Data& data); 41 | }; 42 | ``` 43 | 44 | All Kotlin bindings are registered in the extension module block. In order to expose the member functions of the class `Sample`, we use `native_class` and its builder functions `constructor` and `function`: 45 | ```cpp 46 | DECLARE_NATIVE_CLASS(Sample, "com.kheiron.example.Sample") 47 | 48 | JAVA_EXTENSION_MODULE() { 49 | using namespace java; 50 | native_class() 51 | .constructor("create") 52 | .constructor("create") 53 | .function<&Sample::get_data>("get_data") 54 | .function<&Sample::set_data>("set_data") 55 | ; 56 | print_registered_bindings(); 57 | } 58 | ``` 59 | The type parameter of the template function `constructor` is a function signature to help choose between multiple available constructors (between constructors that take parameter types `const char*` and `std::string` in this case). The non-type template parameter of `function` is a function pointer, either a member function pointer (as shown above) or a free function pointer. 60 | 61 | `print_registered_bindings` is a utility function that lets you print the Kotlin class definition that corresponds to the registered C++ class definitions. `print_registered_bindings` prints to Java `System.out` when you load the compiled shared library (`*.so`) with Kotlin's `System.load()`. You would normally use it in the development phase. 62 | 63 | The bindings above map to the following class definition in Kotlin: 64 | ```kotlin 65 | package com.kheiron.example 66 | import com.kheiron.ktbind.NativeObject 67 | 68 | class Sample private constructor() : NativeObject() { 69 | external override fun close() 70 | external fun get_data(): Data 71 | external fun set_data(data: Data) 72 | companion object { 73 | @JvmStatic external fun create(): Sample 74 | @JvmStatic external fun create(str: String): Sample 75 | } 76 | } 77 | ``` 78 | 79 | Notice that the Kotlin functions corresponding to the C++ constructors appear in the Kotlin companion object, and the Kotlin class itself has only a private constructor. This highlights an important characteristic of `native_class`: it serves as a way to expose native objects to Kotlin. The native object lives in the C++ space, and `native_class` exposes an opaque handle to the object. (This opaque handle is stored in `NativeObject`.) Whenever a function is called, all parameters are passed by value from Kotlin to C++ by the interoperability layer, and a corresponding function invocation is made on the native object. Let's look at a specific example: 80 | ```kotlin 81 | Sample.create().use { 82 | println(it.get_data()) 83 | it.set_data(Data(true, 42, 65000, 12, 3.1416f, 2.7183, "a Kotlin string", emptyMap())) 84 | } 85 | ``` 86 | 87 | Here, a `Sample` object is instantiated in Kotlin but a corresponding native object is immediately created in the C++ space by calling the default constructor. When a function such as `get_data()` is called, KtBind marshals all parameters, and makes an invocation to `Sample::get_data` defined in C++, and the return value is passed back to Kotlin. 88 | 89 | C++ objects have constructors and destructors but Kotlin (JVM) has garbage collection. In order to ensure that objects are properly reclaimed when they are no longer needed, `Sample` implements the interface `AutoCloseable` (via `NativeObject`). Calling the `close` method triggers the C++ destructor. (In the example, `close()` is called automatically at the end of the `use` block.) 90 | 91 | Notice that we have used `Data`, a type we have yet to define. Its C++ definition looks as follows: 92 | ```cpp 93 | struct Data { 94 | bool b = true; 95 | short s = 82; 96 | int i = 1024; 97 | long l = 111000111000; 98 | float f = M_PI; 99 | double d = M_E; 100 | std::string str = "sample string in struct"; 101 | std::map> map = { 102 | {"one", {"a","b","c"}}, 103 | {"two", {"x","y","z"}}, 104 | {"three", {}}, 105 | {"four", {"l"}} 106 | }; 107 | }; 108 | ``` 109 | 110 | The corresponding Kotlin definition is as shown below: 111 | ```kotlin 112 | data class Data( 113 | val b: Boolean, 114 | val s: Short, 115 | val i: Int, 116 | val l: Long, 117 | val f: Float, 118 | val d: Double, 119 | val str: String, 120 | val map: Map> 121 | ) 122 | ``` 123 | 124 | As shown in the example, KtBind supports fundamental types, object types, generic types, and composite types, up to arbitrary levels of nesting. Data classes are always passed by value, and registered in C++ with `data_class`: 125 | ```cpp 126 | // ... 127 | DECLARE_DATA_CLASS(Data, "com.kheiron.example.Data") 128 | 129 | JAVA_EXTENSION_MODULE() { 130 | // ... 131 | data_class() 132 | .field<&Data::b>("b") 133 | .field<&Data::s>("s") 134 | .field<&Data::i>("i") 135 | .field<&Data::l>("l") 136 | .field<&Data::f>("f") 137 | .field<&Data::d>("d") 138 | .field<&Data::str>("str") 139 | .field<&Data::map>("map") 140 | ; 141 | } 142 | ``` 143 | 144 | Both the C++ and the Kotlin definition of a data class might have fields not registered in the binding but the values of these fields will not be transferred across the language boundary, and will always take their initial values. 145 | 146 | ## Type mapping 147 | 148 | KtBind recognizes several widely-used types and marshals them automatically between C++ and Kotlin without explicit user-defined type specification: 149 | 150 | | C++ type | Kotlin consumed type | Kotlin produced type | 151 | | -------- | -------------------- | -------------------- | 152 | | `void` | n/a | `Unit` | 153 | | `bool` | `Boolean` | `Boolean` | 154 | | `int8_t` | `Byte` | `Byte` | 155 | | `uint16_t` | `Character` | `Character` | 156 | | `int16_t` | `Short` | `Short` | 157 | | `int32_t` | `Int` | `Int` | 158 | | `int64_t` | `Long` | `Long` | 159 | | `int` (32-bit) | `Int` | `Int` | 160 | | `long` (32-bit or 64-bit) | `Int` (for 32-bit) or `Long` (for 64-bit) | `Int` or `Long` | 161 | | `long long` (64-bit) | `Long` | `Long` | 162 | | `unsigned int` (32-bit) | `Int` | `Int` | 163 | | `unsigned long` (32-bit or 64-bit) | `Int` (for 32-bit) or `Long` (for 64-bit) | `Int` or `Long` | 164 | | `unsigned long long` (64-bit) | `Long` | `Long` | 165 | | `float` | `Float` | `Float` | 166 | | `double` | `Double` | `Double` | 167 | | `std::string` (UTF-8) | `String` | `String` | 168 | | `std::wstring` | `String` | `String` | 169 | | `std::vector` if `T` is an arithmetic type | `T[]` | `T[]` | 170 | | `std::vector` if `T` is not an arithmetic type | `java.util.List` | `java.util.ArrayList` | 171 | | `std::list` | `java.util.List` | `java.util.ArrayList` | 172 | | `std::set` | `java.util.Set` | `java.util.TreeSet` | 173 | | `std::unordered_set` | `java.util.Set` | `java.util.HashSet` | 174 | | `std::map` | `java.util.Map` | `java.util.TreeMap` | 175 | | `std::unordered_map` | `java.util.Map` | `java.util.HashMap` | 176 | | `std::function` | `kotlin.jvm.functions.Function`*N* where *N* = number of `Args` | | 177 | 178 | Java boxing an unboxing for types is performed automatically. 179 | 180 | ## Exceptions 181 | 182 | Exceptions thrown in C++ automatically trigger a Java exception when crossing the language boundary. The interoperability layer catches all exceptions that inherit from `std::exception`, and throws a `java.lang.Exception` before passing control back to the JVM. 183 | 184 | Exceptions originating from Java/Kotlin are automatically wrapped in a C++ type called `JavaException`, which derives from `std::exception`. The function `what()` in `JavaException` retrieves the Java exception message. C++ code can catch `JavaException` and take appropriate action, which causes the exception to be cleared in Java. 185 | 186 | ## Callbacks 187 | 188 | Passing a callback or lambda from Kotlin to C++ is fully supported. The callback or lambda is wrapped in a `std::function String): String 194 | } 195 | } 196 | ``` 197 | and the way it is used: 198 | ```kotlin 199 | val result = LambdaSample.pass_callback_arguments("callback") { str, short, int, long -> "($str, $short, $int, $long)" } 200 | ``` 201 | The corresponding C++ function definition looks as follows: 202 | ``` 203 | std::string pass_callback_arguments(std::string str, std::function fun) { 204 | return fun(str, 4, 82, 112); 205 | } 206 | ``` 207 | In the example above, `result` evaluates to `"(callback, 4, 82, 112)"`. 208 | 209 | ## Binding registration 210 | 211 | The macro `JAVA_EXTENSION_MODULE` in KtBind expands into a pair of function definitions: 212 | ```c 213 | JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { ... } 214 | JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) { ... } 215 | ``` 216 | These definitions, in turn, iterate over the function and field bindings registered with `native_class` and `data_class`. 217 | 218 | Each function binding generates a function pointer at compile time, which are passed to the JNI function `RegisterNatives`. Each of these function pointers points at a static member function of a template class, where the template parameters capture the type information extracted from the function signature. When the function is invoked through the pointer, the function makes the appropriate type conversions to cast Java types into C++ types and back. For example, the C++ function signature 219 | ```cpp 220 | bool func(const std::string& str, const std::vector& vec, double d); 221 | ``` 222 | causes the function adapter template to be instantiated with parameter types `std::string`, `std::vector` and `double` and return type `bool`. When Java calls the pointer through JNI, the adapter transforms the types `std::string` and `std::vector`. (`double` and `bool` need no transformation.) For each transformed type, a temporary object is created, all of which are then used in invoking the original function `func`. 223 | 224 | Internally, field bindings utilize JNI accessor functions like `GetObjectField` and `SetObjectField` to extract and populate Java objects. Like with function bindinds, KtBind uses C++ type information to make the appropriate JNI function call. For instance, setting a field with type `double` entails a call to `GetDoubleField` (from Java to C++) or `SetDoubleField` (from C++ to Java). If the type is a composite type, such as a `std::vector`, then a Java object is constructed recursively, and then set with `SetObjectField`. For example, 225 | * Setting a field of type `std::vector` first creates a `java.util.ArrayList` with JNI's `NewObject`, then sets elements with the `add` method (invoked using JNI's `CallBooleanMethod`), performing boxing for the primitive type `int` with `valueOf`, and finally uses `SetObjectField` with the newly created `java.util.ArrayList` instance. 226 | * Setting an `std::vector` field involves creating a `java.util.ArrayList` with JNI's `NewObject`, and a call to JNI's `NewStringUTF` for each string element. The strings are then added to the `java.util.ArrayList` instance with `add`, and finally to the field with `SetObjectField`. 227 | -------------------------------------------------------------------------------- /kotlin/src/test/kotlin/com/kheiron/ktbind/NativeBindingsTest.kt: -------------------------------------------------------------------------------- 1 | package com.kheiron.ktbind 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.assertThrows 6 | import java.io.ByteArrayOutputStream 7 | import java.io.File 8 | import java.io.PrintStream 9 | import kotlin.concurrent.thread 10 | 11 | /** 12 | * Represents a class that is instantiated in native code. 13 | */ 14 | abstract class NativeObject : AutoCloseable { 15 | /** 16 | * Holds a reference to an object that exists in the native code execution context. 17 | */ 18 | @Suppress("unused") 19 | private val nativePointer: Long = 0 20 | } 21 | 22 | data class Data( 23 | val b: Boolean = false, 24 | val s: Short = 0, 25 | val i: Int = 0, 26 | val l: Long = 0, 27 | val f: Float = 0.0f, 28 | val d: Double = 0.0, 29 | val str: String = "", 30 | val short_arr: ShortArray = ShortArray(0), 31 | val int_arr: IntArray = IntArray(0), 32 | val long_arr: LongArray = LongArray(0), 33 | val map: Map> = emptyMap() 34 | ) 35 | 36 | class Sample private constructor() : NativeObject() { 37 | external override fun close() 38 | external fun get_data(): Data 39 | external fun set_data(data: Data) 40 | external fun duplicate(): Sample 41 | companion object { 42 | @JvmStatic external fun create(): Sample 43 | @JvmStatic external fun create(str: String): Sample 44 | @JvmStatic external fun returns_void() 45 | @JvmStatic external fun returns_bool(): Boolean 46 | @JvmStatic external fun returns_short(): Short 47 | @JvmStatic external fun returns_int(): Int 48 | @JvmStatic external fun returns_long(): Long 49 | @JvmStatic external fun returns_int16(): Short 50 | @JvmStatic external fun returns_int32(): Int 51 | @JvmStatic external fun returns_int64(): Long 52 | @JvmStatic external fun returns_float(): Float 53 | @JvmStatic external fun returns_double(): Double 54 | @JvmStatic external fun returns_string(): String 55 | @JvmStatic external fun pass_arguments_by_value(str: String, b: Boolean, s: Short, i: Int, l: Long, i16: Short, i32: Int, i64: Long, f: Float, d: Double): Boolean 56 | @JvmStatic external fun pass_arguments_by_reference(str: String, b: Boolean, s: Short, i: Int, l: Long, i16: Short, i32: Int, i64: Long, f: Float, d: Double): Boolean 57 | @JvmStatic external fun array_of_char(list: ByteArray): ByteArray 58 | @JvmStatic external fun array_of_int(list: IntArray): IntArray 59 | @JvmStatic external fun array_of_string(list: List): List 60 | @JvmStatic external fun list_of_int(list: List): List 61 | @JvmStatic external fun list_of_string(list: List): List 62 | @JvmStatic external fun unordered_set(set: Set): Set 63 | @JvmStatic external fun ordered_set(set: Set): Set 64 | @JvmStatic external fun unordered_map(map: Map): Map 65 | @JvmStatic external fun ordered_map_of_int(map: Map): Map 66 | @JvmStatic external fun ordered_map_of_string(map: Map): Map 67 | @JvmStatic external fun native_composite(map: Map>): Map> 68 | @JvmStatic external fun pass_callback(callback: () -> Unit) 69 | @JvmStatic external fun pass_callback_returns_string(callback: () -> String): String 70 | @JvmStatic external fun pass_callback_string_returns_int(str: String, callback: (String) -> Int): Int 71 | @JvmStatic external fun pass_callback_string_returns_string(str: String, callback: (String) -> String): String 72 | @JvmStatic external fun pass_callback_arguments(str: String, callback: (String, Short, Int, Long) -> String): String 73 | @JvmStatic external fun callback_on_native_thread(callback: () -> Unit) 74 | @JvmStatic external fun raise_native_exception() 75 | @JvmStatic external fun catch_java_exception(callback: () -> Unit) 76 | } 77 | } 78 | 79 | fun captureOutput(executable: () -> Unit): String { 80 | return ByteArrayOutputStream().use { stream -> 81 | val stdout = System.out 82 | try { 83 | System.setOut(PrintStream(stream)) 84 | executable() 85 | } finally { 86 | System.setOut(stdout) 87 | } 88 | stream.toString() 89 | }.trimEnd() 90 | } 91 | 92 | fun assertPrints(expected: String, executable: () -> Unit) { 93 | assertEquals(expected, captureOutput(executable)) 94 | } 95 | 96 | fun assertPrints(expected: Regex, executable: () -> Unit) { 97 | assertTrue(expected.matches(captureOutput(executable))) 98 | } 99 | 100 | internal class NativeBindingsTest { 101 | init { 102 | val libraryFile = File(System.getProperty("user.dir"), "../cpp/build/libktbind_java.so").absolutePath 103 | System.load(libraryFile) 104 | } 105 | 106 | @Test 107 | fun `lambda signatures`() { 108 | val fn: (String, Int, Long) -> String = { str, int, long -> "$str = $int + $long" } 109 | val c: Class<*> = fn::class.java 110 | println(c.name) 111 | for (classInterface in c.interfaces) { 112 | println(classInterface) 113 | } 114 | for (method in c.declaredMethods) { 115 | val methodName = method.name 116 | val signature = method.parameters.joinToString(separator = ", ") { 117 | "${it.name}: ${it.type}" 118 | } 119 | val returnType = method.returnType 120 | println("$methodName($signature): $returnType") 121 | } 122 | } 123 | 124 | /** 125 | * Tests fundamental and well-known types. 126 | */ 127 | @Test 128 | fun `fundamental types`() { 129 | Sample.returns_void() 130 | assertEquals(true, Sample.returns_bool()) 131 | assertEquals(32767, Sample.returns_short()) 132 | assertEquals(2147483647, Sample.returns_int()) 133 | assertEquals(9223372036854775807, Sample.returns_long()) 134 | assertEquals(32767, Sample.returns_int16()) 135 | assertEquals(2147483647, Sample.returns_int32()) 136 | assertEquals(9223372036854775807, Sample.returns_int64()) 137 | assertEquals(3.4028235E38f, Sample.returns_float()) 138 | assertEquals(1.7976931348623157E308, Sample.returns_double()) 139 | assertEquals("a sample string", Sample.returns_string()) 140 | 141 | val ref = "(string = string, bool = 1, short = 32000, int = 128000, long = 1000000000, int16_t = 32000, int32_t = 128000, int64_t = 1000000000, float = 3.14, double = 3.14)" 142 | assertPrints(ref) { 143 | Sample.pass_arguments_by_value("string", true, 32000, 128000, 1000000000, 32000, 128000, 1000000000, 3.14f, 3.14) 144 | } 145 | assertPrints(ref) { 146 | Sample.pass_arguments_by_reference("string", true, 32000, 128000, 1000000000, 32000, 128000, 1000000000, 3.14f, 3.14) 147 | } 148 | } 149 | 150 | @Test 151 | fun `passing and returning collections`() { 152 | assertPrints("[$, a, b, c]") { 153 | assertArrayEquals(charArrayOf('a', 'b', 'c', 'd', 'e', 'f').map { it.toByte() }.toByteArray(), Sample.array_of_char(byteArrayOf('$'.toByte(), 'a'.toByte(), 'b'.toByte(), 'c'.toByte()))) 154 | } 155 | assertPrints("[0, 0, 1, 1, 2, 3, 5, 8, 13]") { 156 | assertArrayEquals(intArrayOf(0, 1, 2, 3, 4, 5, 6), Sample.array_of_int(intArrayOf(0, 0, 1, 1, 2, 3, 5, 8, 13))) 157 | } 158 | assertPrints("[$, a, A, b, B, c, C]") { 159 | assertIterableEquals(listOf("", "A", "B", "C", "D", "E", "F"), Sample.array_of_string(listOf("$", "a", "A", "b", "B", "c", "C"))) 160 | } 161 | assertPrints("[1, 1, 2, 3, 5, 8, 13]") { 162 | assertIterableEquals(listOf(1, 2, 3, 4, 5, 6), Sample.list_of_int(listOf(1, 1, 2, 3, 5, 8, 13))) 163 | } 164 | assertPrints("[a, A, b, B, c, C]") { 165 | assertIterableEquals(listOf("A", "B", "C", "D", "E", "F"), Sample.list_of_string(listOf("a", "A", "b", "B", "c", "C"))) 166 | } 167 | assertPrints(Regex("""\{(a|b|c), (a|b|c), (a|b|c)}""")) { 168 | assertTrue(setOf("A", "B", "C") == Sample.unordered_set(setOf("a", "b", "c"))) 169 | } 170 | assertPrints("{a, b, c}") { 171 | assertTrue(setOf("A", "B", "C") == Sample.ordered_set(setOf("a", "b", "c"))) 172 | } 173 | assertPrints(Regex("""\{(a: b|b: c|c: d), (a: b|b: c|c: d), (a: b|b: c|c: d)}""")) { 174 | assertTrue(mapOf("1" to "A", "2" to "B", "3" to "C") == Sample.unordered_map(mapOf("a" to "b", "b" to "c", "c" to "d"))) 175 | } 176 | assertPrints("{1: 2, 2: 3, 3: 4}") { 177 | assertTrue(mapOf(1L to 1000L, 2L to 2000L, 3L to 3000L) == Sample.ordered_map_of_int(mapOf(1L to 2L, 2L to 3L, 3L to 4L))) 178 | } 179 | assertPrints("{a: b, b: c, c: d}") { 180 | assertTrue(mapOf("1" to "A", "2" to "B", "3" to "C") == Sample.ordered_map_of_string(mapOf("a" to "b", "b" to "c", "c" to "d"))) 181 | } 182 | assertPrints("{a: [1, 2, 3], b: [], c: [4]}") { 183 | val result = Sample.native_composite(mapOf("a" to listOf("1", "2", "3"), "b" to emptyList(), "c" to listOf("4"))) 184 | assertIterableEquals(listOf("a", "b", "c"), result["A"]); 185 | assertIterableEquals(emptyList(), result["B"]); 186 | assertIterableEquals(listOf("x"), result["C"]) 187 | } 188 | } 189 | 190 | @Test 191 | fun `callback functions`() { 192 | assertPrints("void callback") { 193 | Sample.pass_callback { println("void callback") } 194 | } 195 | assertEquals("alma", Sample.pass_callback_returns_string { "alma" }) 196 | assertEquals(82, Sample.pass_callback_string_returns_int("callback") { 82 }) 197 | assertEquals("cpp(callback) -> kotlin(callback)", 198 | Sample.pass_callback_string_returns_string("callback") { "cpp($it) -> kotlin($it)" }) 199 | assertEquals("(callback, 4, 82, 112)", 200 | Sample.pass_callback_arguments("callback") { str, short, int, long -> "($str, $short, $int, $long)" }) 201 | 202 | // callback functions from new thread 203 | thread { 204 | assertPrints("[1, 1, 2, 3, 5, 8, 13]") { 205 | assertIterableEquals(listOf(1, 2, 3, 4, 5, 6), Sample.list_of_int(listOf(1, 1, 2, 3, 5, 8, 13))) 206 | } 207 | assertPrints("void callback") { 208 | Sample.pass_callback { println("void callback") } 209 | } 210 | assertEquals("alma", Sample.pass_callback_returns_string { "alma" }) 211 | assertEquals(82, Sample.pass_callback_string_returns_int("callback") { 82 }) 212 | assertEquals("cpp(callback) -> kotlin(callback)", 213 | Sample.pass_callback_string_returns_string("callback") { "cpp($it) -> kotlin($it)" }) 214 | assertEquals("(callback, 4, 82, 112)", 215 | Sample.pass_callback_arguments("callback") { str, short, int, long -> "($str, $short, $int, $long)" }) 216 | }.join() 217 | 218 | Sample.callback_on_native_thread { println("executed on native thread") } 219 | } 220 | 221 | @Test 222 | fun `native and java exceptions`() { 223 | assertThrows { 224 | Sample.raise_native_exception() 225 | } 226 | assertThrows { 227 | Sample.pass_callback { throw Exception("non-critical error") } 228 | } 229 | assertPrints("exception caught: non-critical error") { 230 | assertDoesNotThrow { 231 | Sample.catch_java_exception { throw Exception("non-critical error") } 232 | } 233 | } 234 | } 235 | 236 | @Test 237 | fun `object creation`() { 238 | assertPrints(""" 239 | created 240 | get nested data 241 | Kotlin: Data(b=true, s=82, i=1024, l=111000111000, f=3.1415927, d=2.718281828459045, str=sample string in struct, short_arr=[1, 2, 3, 4, 5, 6, 7, 8, 9], int_arr=[10, 20, 30, 40, 50, 60, 70, 80, 90], long_arr=[100, 200, 300, 400, 500, 600, 700, 800, 900], map={four=[l], one=[a, b, c], three=[], two=[x, y, z]}) 242 | set nested data: {b=1, s=42, i=65000, l=12, f=3.14, d=3.14, str='a Kotlin string'} 243 | destroyed 244 | """.trimIndent()) { 245 | Sample.create().use { 246 | println("Kotlin: ${it.get_data()}") 247 | it.set_data(Data(true, 42, 65000, 12, 3.14f, 3.14, "a Kotlin string", ShortArray(0), IntArray(0), LongArray(0), emptyMap())) 248 | } 249 | } 250 | 251 | assertPrints(""" 252 | created from string 253 | destroyed 254 | """.trimIndent()) { 255 | Sample.create("parameter").use {} 256 | } 257 | 258 | Sample.create().use { 259 | val other = it.duplicate() 260 | it.set_data(Data()) 261 | val data = it.get_data() 262 | assertEquals(false, data.b) 263 | assertEquals(0, data.s) 264 | assertEquals(0, data.i) 265 | assertEquals(0, data.l) 266 | assertEquals("", data.str) 267 | assertTrue(data.map.isEmpty()) 268 | val data_other = other.get_data() 269 | assertEquals(true, data_other.b) 270 | assertEquals(82, data_other.s) 271 | assertEquals(1024, data_other.i) 272 | assertEquals(111000111000, data_other.l) 273 | assertEquals("sample string in struct", data_other.str) 274 | assertFalse(data_other.map.isEmpty()) 275 | other.close() 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /cpp/include/ktbind/ktbind.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if __cplusplus < 201703L 4 | #error Including requires building with -std=c++17 or newer. 5 | #endif 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | namespace java { 27 | /** 28 | * Builds a zero-terminated string literal from an std::array. 29 | * @tparam N The size of the std::array. 30 | * @tparam I An index sequence, typically constructed with std::make_index_sequence. 31 | */ 32 | template const& S, typename I> 33 | struct to_char_array; 34 | 35 | template const& S, std::size_t... I> 36 | struct to_char_array> { 37 | static constexpr const char value[] { S[I]..., 0 }; 38 | }; 39 | 40 | /** 41 | * Returns the number of digits in n. 42 | */ 43 | constexpr std::size_t num_digits(std::size_t n) { 44 | return n < 10 ? 1 : num_digits(n / 10) + 1; 45 | } 46 | 47 | /** 48 | * Converts an unsigned integer into sequence of decimal digits. 49 | */ 50 | template 51 | struct integer_to_digits { 52 | private: 53 | constexpr static std::size_t len = num_digits(N); 54 | constexpr static auto impl() { 55 | std::array arr{}; 56 | std::size_t n = N; 57 | std::size_t i = len; 58 | while (i > 0) { 59 | --i; 60 | arr[i] = '0' + (n % 10); 61 | n /= 10; 62 | } 63 | return arr; 64 | } 65 | constexpr static auto arr = impl(); 66 | 67 | public: 68 | constexpr static std::string_view value = std::string_view( 69 | to_char_array< arr.size(), arr, std::make_index_sequence >::value, 70 | arr.size() 71 | ); 72 | }; 73 | 74 | /** 75 | * Replaces all occurrences of a character in a string with another character at compile time. 76 | * @tparam S The string in which replacements are made. 77 | * @tparam O The character to look for. 78 | * @tparam N The character to replace to. 79 | */ 80 | template 81 | class replace { 82 | static constexpr auto impl() noexcept { 83 | std::array arr{}; 84 | for (std::size_t i = 0; i < S.size(); ++i) { 85 | if (S[i] == O) { 86 | arr[i] = N; 87 | } else { 88 | arr[i] = S[i]; 89 | } 90 | } 91 | return arr; 92 | } 93 | 94 | static constexpr auto arr = impl(); 95 | 96 | public: 97 | static constexpr std::string_view value = std::string_view( 98 | to_char_array< arr.size(), arr, std::make_index_sequence >::value, 99 | arr.size() 100 | ); 101 | }; 102 | 103 | template 104 | static constexpr auto replace_v = replace::value; 105 | 106 | /** 107 | * Concatenates a list of strings at compile time. 108 | */ 109 | template 110 | class join { 111 | // join all strings into a single std::array of chars 112 | static constexpr auto impl() noexcept { 113 | constexpr std::size_t len = (Strs.size() + ... + 0); 114 | std::array arr{}; 115 | auto append = [i = 0, &arr](auto const& s) mutable { 116 | for (auto c : s) { 117 | arr[i++] = c; 118 | } 119 | }; 120 | (append(Strs), ...); 121 | return arr; 122 | } 123 | 124 | // give the joined string static storage 125 | static constexpr auto arr = impl(); 126 | 127 | public: 128 | // convert to a string literal, then view as a std::string_view 129 | static constexpr std::string_view value = std::string_view( 130 | to_char_array< arr.size(), arr, std::make_index_sequence >::value, 131 | arr.size() 132 | ); 133 | }; 134 | 135 | template 136 | static constexpr auto join_v = join::value; 137 | 138 | /** 139 | * Concatenates a list of strings at compile time, inserting a separator between neighboring items. 140 | */ 141 | template 142 | struct join_sep; 143 | 144 | template 145 | struct join_sep { 146 | constexpr static std::string_view value = join::value...>::value; 147 | }; 148 | 149 | template 150 | struct join_sep { 151 | constexpr static std::string_view value = Item; 152 | }; 153 | 154 | template 155 | struct join_sep { 156 | constexpr static std::string_view value = ""; 157 | }; 158 | 159 | // helper to extract value 160 | template 161 | static constexpr auto join_sep_v = join_sep::value; 162 | 163 | /** 164 | * Allows a friendly message to be built with the stream insertion operator. 165 | * 166 | * Example: throw std::runtime_error(msg() << "Error: " << code); 167 | */ 168 | struct msg { 169 | template 170 | msg& operator<<(const T& part) { 171 | str << part; 172 | return *this; 173 | } 174 | 175 | template 176 | msg& operator<<(T&& part) { 177 | str << part; 178 | return *this; 179 | } 180 | 181 | operator std::string() const { 182 | return str.str(); 183 | } 184 | 185 | private: 186 | std::ostringstream str; 187 | }; 188 | 189 | /** 190 | * An exception that originates from Java. 191 | */ 192 | struct JavaException : std::exception { 193 | JavaException(JNIEnv* env) { 194 | if (env->ExceptionCheck()) { 195 | ex = env->ExceptionOccurred(); 196 | 197 | // clear the exception to allow calling JNI functions 198 | env->ExceptionClear(); 199 | 200 | // extract exception message using low-level functions 201 | jclass exceptionClass = env->GetObjectClass(ex); 202 | jmethodID getMessageFunc = env->GetMethodID(exceptionClass, "getMessage", "()Ljava/lang/String;"); 203 | jstring messageObject = static_cast(env->CallObjectMethod(ex, getMessageFunc)); 204 | const char* c_str = env->GetStringUTFChars(messageObject, nullptr); 205 | message = c_str; 206 | env->ReleaseStringUTFChars(messageObject, c_str); 207 | env->DeleteLocalRef(messageObject); 208 | env->DeleteLocalRef(exceptionClass); 209 | } 210 | } 211 | 212 | /** 213 | * Exceptions must not be copied as they contain a JNI local reference. 214 | */ 215 | JavaException(const JavaException&) = delete; 216 | 217 | const char* what() const noexcept { 218 | return message.c_str(); 219 | } 220 | 221 | /** 222 | * Used by the interoperability framework to re-throw the exception in Java before crossing the native to Java 223 | * boundary, unless the exception has been caught by the user. 224 | */ 225 | jthrowable innerException() const noexcept { 226 | return ex; 227 | } 228 | 229 | private: 230 | jthrowable ex = nullptr; 231 | std::string message; 232 | }; 233 | 234 | class LocalClassRef; 235 | 236 | /** 237 | * C++ wrapper class of [jmethodID] for instance methods. 238 | */ 239 | class Method { 240 | Method(JNIEnv* env, jclass cls, const char* name, const std::string_view& signature) { 241 | _ref = env->GetMethodID(cls, name, signature.data()); 242 | if (_ref == nullptr) { 243 | throw JavaException(env); // method not found 244 | } 245 | } 246 | 247 | friend LocalClassRef; 248 | 249 | jmethodID _ref = nullptr; 250 | 251 | public: 252 | Method() = default; 253 | 254 | jmethodID ref() const { 255 | return _ref; 256 | } 257 | }; 258 | 259 | /** 260 | * C++ wrapper class of [jmethodID] for class methods. 261 | */ 262 | class StaticMethod { 263 | StaticMethod(JNIEnv* env, jclass cls, const char* name, const std::string_view& signature) { 264 | _ref = env->GetStaticMethodID(cls, name, signature.data()); 265 | if (_ref == nullptr) { 266 | throw JavaException(env); // method not found 267 | } 268 | } 269 | 270 | friend LocalClassRef; 271 | 272 | jmethodID _ref = nullptr; 273 | 274 | public: 275 | StaticMethod() = default; 276 | 277 | jmethodID ref() const { 278 | return _ref; 279 | } 280 | }; 281 | 282 | /** 283 | * C++ wrapper class of [jfieldID] for instance fields. 284 | */ 285 | class Field { 286 | Field(JNIEnv* env, jclass cls, const char* name, const std::string_view& signature) { 287 | _ref = env->GetFieldID(cls, name, signature.data()); 288 | if (_ref == nullptr) { 289 | throw JavaException(env); // field not found 290 | } 291 | } 292 | 293 | friend LocalClassRef; 294 | 295 | jfieldID _ref = nullptr; 296 | 297 | public: 298 | Field() = default; 299 | 300 | jfieldID ref() const { 301 | return _ref; 302 | } 303 | }; 304 | 305 | /** 306 | * C++ wrapper class of [jfieldID] for class fields. 307 | */ 308 | class StaticField { 309 | StaticField(JNIEnv* env, jclass cls, const char* name, const std::string_view& signature) { 310 | _ref = env->GetStaticFieldID(cls, name, signature.data()); 311 | if (_ref == nullptr) { 312 | throw JavaException(env); // field not found 313 | } 314 | } 315 | 316 | friend LocalClassRef; 317 | 318 | jfieldID _ref = nullptr; 319 | 320 | public: 321 | StaticField() = default; 322 | 323 | jfieldID ref() const { 324 | return _ref; 325 | } 326 | }; 327 | 328 | /** 329 | * Scoped C++ wrapper class of a [jobject] that is used only within a single native execution block. 330 | */ 331 | class LocalObjectRef { 332 | public: 333 | LocalObjectRef() = default; 334 | LocalObjectRef(JNIEnv* env, jobject obj) : _env(env), _ref(obj) {} 335 | 336 | LocalObjectRef(const LocalObjectRef& op) = delete; 337 | LocalObjectRef& operator=(const LocalObjectRef& op) = delete; 338 | 339 | LocalObjectRef(LocalObjectRef&& op) : _env(op._env), _ref(op._ref) { 340 | op._ref = nullptr; 341 | } 342 | 343 | ~LocalObjectRef() { 344 | if (_ref != nullptr) { 345 | _env->DeleteLocalRef(_ref); 346 | } 347 | } 348 | 349 | jobject ref() const { 350 | return _ref; 351 | } 352 | 353 | private: 354 | JNIEnv* _env = nullptr; 355 | jobject _ref = nullptr; 356 | }; 357 | 358 | /** 359 | * Scoped C++ wrapper class of [jclass]. 360 | */ 361 | class LocalClassRef { 362 | public: 363 | LocalClassRef(JNIEnv* env, const char* name) : LocalClassRef(env, name, std::nothrow) { 364 | if (_ref == nullptr) { 365 | throw JavaException(env); // Java class not found 366 | } 367 | } 368 | 369 | LocalClassRef(JNIEnv* env, const char* name, std::nothrow_t) : _env(env) { 370 | _ref = env->FindClass(name); 371 | } 372 | 373 | LocalClassRef(JNIEnv* env, jobject obj) : _env(env) { 374 | _ref = env->GetObjectClass(obj); 375 | } 376 | 377 | LocalClassRef(JNIEnv* env, jclass cls) : _env(env), _ref(cls) {} 378 | 379 | ~LocalClassRef() { 380 | if (_ref != nullptr) { 381 | _env->DeleteLocalRef(_ref); 382 | } 383 | } 384 | 385 | LocalClassRef(const LocalClassRef&) = delete; 386 | LocalClassRef& operator=(const LocalClassRef& op) = delete; 387 | 388 | LocalClassRef(LocalClassRef&& op) : _env(op._env), _ref(op._ref) { 389 | op._ref = nullptr; 390 | } 391 | 392 | Method getMethod(const char* name, const std::string_view& signature) { 393 | return Method(_env, _ref, name, signature); 394 | } 395 | 396 | Field getField(const char* name, const std::string_view& signature) { 397 | return Field(_env, _ref, name, signature); 398 | } 399 | 400 | StaticMethod getStaticMethod(const char* name, const std::string_view& signature) { 401 | return StaticMethod(_env, _ref, name, signature); 402 | } 403 | 404 | StaticField getStaticField(const char* name, const std::string_view& signature) { 405 | return StaticField(_env, _ref, name, signature); 406 | } 407 | 408 | LocalObjectRef getStaticObjectField(const char* name, const std::string_view& signature) { 409 | StaticField fld = getStaticField(name, signature); 410 | return LocalObjectRef(_env, _env->GetStaticObjectField(_ref, fld.ref())); 411 | } 412 | 413 | jclass ref() const { 414 | return _ref; 415 | } 416 | 417 | private: 418 | JNIEnv* _env; 419 | jclass _ref; 420 | }; 421 | 422 | /** 423 | * Represents the JNI environment in which the extension module is executing. 424 | */ 425 | struct Environment { 426 | /** Triggered by the function `JNI_OnLoad`. */ 427 | static void load(JavaVM* vm) { 428 | assert(_vm == nullptr); 429 | _vm = vm; 430 | } 431 | 432 | /** Triggered by the function `JNI_OnUnload`. */ 433 | static void unload(JavaVM* vm) { 434 | assert(_vm != nullptr); 435 | _vm = nullptr; 436 | } 437 | 438 | void setEnv(JNIEnv* env) { 439 | assert(_vm != nullptr); 440 | assert(_env == nullptr || _env == env); 441 | _env = env; 442 | } 443 | 444 | JNIEnv* getEnv() { 445 | assert(_vm != nullptr); 446 | 447 | if (_env == nullptr) { 448 | // attach thread to obtain an environment 449 | switch (_vm->GetEnv(reinterpret_cast(&_env), JNI_VERSION_1_6)) { 450 | case JNI_OK: 451 | break; 452 | case JNI_EDETACHED: 453 | if (_vm->AttachCurrentThread(reinterpret_cast(&_env), nullptr) == JNI_OK) { 454 | assert(_env != nullptr); 455 | _attached = true; 456 | } else { 457 | // failed to attach thread 458 | return nullptr; 459 | } 460 | break; 461 | case JNI_EVERSION: 462 | default: 463 | // unsupported JVM version or other error 464 | return nullptr; 465 | } 466 | } 467 | 468 | return _env; 469 | } 470 | 471 | ~Environment() { 472 | if (!_env) { 473 | return; 474 | } 475 | 476 | // only threads explicitly attached by native code should be released 477 | if (_vm != nullptr && _attached) { 478 | // detach thread 479 | _vm->DetachCurrentThread(); 480 | } 481 | } 482 | 483 | private: 484 | inline static JavaVM* _vm = nullptr; 485 | JNIEnv* _env = nullptr; 486 | bool _attached = false; 487 | }; 488 | 489 | /** 490 | * Ensures that Java resources allocated by the thread are released when the thread terminates. 491 | */ 492 | static thread_local Environment this_thread; 493 | 494 | /** 495 | * An adapter for an object reference handle that remains valid as the native-to-Java boundary is crossed. 496 | */ 497 | class GlobalObjectRef { 498 | public: 499 | using jobject_struct = std::pointer_traits::element_type; 500 | 501 | GlobalObjectRef(JNIEnv* env, jobject obj) { 502 | _ref = std::shared_ptr(env->NewGlobalRef(obj), [](jobject ref) { 503 | JNIEnv* env = this_thread.getEnv(); 504 | if (env != nullptr) { 505 | env->DeleteGlobalRef(ref); 506 | } 507 | }); 508 | } 509 | 510 | jobject ref() const { 511 | return _ref.get(); 512 | } 513 | 514 | private: 515 | std::shared_ptr _ref; 516 | }; 517 | 518 | /** 519 | * Used in static_assert to have the type name printed in the compiler error message. 520 | */ 521 | template 522 | struct fail : std::false_type {}; 523 | 524 | /** 525 | * Argument type traits, and argument type conversion between native and Java. 526 | * 527 | * Template substitution fails automatically unless the type is a well-known type or has been declared 528 | * with DECLARE_DATA_CLASS or DECLARE_NATIVE_CLASS. 529 | */ 530 | template 531 | struct ArgType { 532 | using native_type = void; 533 | using java_type = void; 534 | 535 | // unused, included to suppress compiler error messages 536 | constexpr static std::string_view kotlin_type = ""; 537 | constexpr static std::string_view type_sig = ""; 538 | 539 | // returns the corresponding array type for the base type 540 | constexpr static std::string_view array_type_prefix = "["; 541 | constexpr static std::string_view array_type_sig = join_v::type_sig>; 542 | 543 | // unused, included to suppress compiler error messages 544 | static void native_value(JNIEnv*, jobject value) {} 545 | static void java_value(JNIEnv* env, const T& value) {} 546 | 547 | static_assert(fail::value, "Unrecognized type detected, ensure that all involved types have been declared as a binding type with DECLARE_DATA_CLASS or DECLARE_NATIVE_CLASS."); 548 | }; 549 | 550 | /** 551 | * Base class for converting primitive types such as [int] or [double]. 552 | */ 553 | template 554 | struct FundamentalArgType { 555 | using native_type = T; 556 | using java_type = J; 557 | 558 | private: 559 | constexpr static std::string_view lparen = "("; 560 | constexpr static std::string_view rparen = ")"; 561 | constexpr static std::string_view class_type_prefix = "L"; 562 | constexpr static std::string_view class_type_suffix = ";"; 563 | 564 | /** Used in looking up the appropriate `valueOf()` function to instantiate object wrappers of values of a primitive type. */ 565 | constexpr static std::string_view value_initializer = join_v::type_sig, rparen, class_type_prefix, ArgType::class_name, class_type_suffix>; 566 | 567 | /** Used in looking up the appropriate `primitiveValue()` function to fetch the primitive type (e.g. `intValue()` for `int`). */ 568 | constexpr static std::string_view get_value_func_suffix = "Value"; 569 | constexpr static std::string_view get_value_func = join_v::primitive_type, get_value_func_suffix>; 570 | constexpr static std::string_view get_value_func_sig = join_v::type_sig>; 571 | 572 | public: 573 | static T native_value(JNIEnv*, J value) { 574 | return static_cast(value); 575 | } 576 | 577 | static J java_value(JNIEnv* env, T value) { 578 | return static_cast(value); 579 | } 580 | 581 | /** 582 | * Wraps the primitive type (e.g. int) into an object type (e.g. Integer). 583 | */ 584 | static jobject java_box(JNIEnv* env, J value) { 585 | LocalClassRef cls(env, ArgType::class_name.data()); 586 | StaticMethod valueOf = cls.getStaticMethod("valueOf", value_initializer); 587 | return env->CallStaticObjectMethod(cls.ref(), valueOf.ref(), value); 588 | } 589 | 590 | /** 591 | * Unwraps a primitive type (e.g. int) from an object type (e.g. Integer). 592 | */ 593 | static J java_unbox(JNIEnv* env, jobject obj) { 594 | LocalClassRef cls(env, obj); 595 | Method getValue = cls.getMethod(get_value_func.data(), get_value_func_sig); 596 | return ArgType::java_call_method(env, obj, getValue); 597 | } 598 | 599 | /** 600 | * Extracts a native value from a Java object field. 601 | */ 602 | static T native_field_value(JNIEnv* env, jobject obj, Field& fld) { 603 | return ArgType::native_value(env, ArgType::java_raw_field_value(env, obj, fld)); 604 | } 605 | 606 | /** 607 | * Creates a Java array from a range of native values. 608 | * To be implemented by concrete types. 609 | */ 610 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len); 611 | }; 612 | 613 | template <> 614 | struct ArgType { 615 | constexpr static std::string_view kotlin_type = "Unit"; 616 | constexpr static std::string_view type_sig = "V"; 617 | using native_type = void; 618 | using java_type = void; 619 | }; 620 | 621 | template <> 622 | struct ArgType : FundamentalArgType { 623 | constexpr static std::string_view class_name = "java/lang/Boolean"; 624 | constexpr static std::string_view primitive_type = "boolean"; 625 | constexpr static std::string_view kotlin_type = "Boolean"; 626 | constexpr static std::string_view type_sig = "Z"; 627 | 628 | static jboolean java_call_method(JNIEnv* env, jobject obj, Method& m) { 629 | return env->CallBooleanMethod(obj, m.ref()); 630 | } 631 | 632 | static jboolean java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 633 | return env->GetBooleanField(obj, fld.ref()); 634 | } 635 | 636 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 637 | env->SetBooleanField(obj, fld.ref(), java_value(env, value)); 638 | } 639 | 640 | static void native_array_value(JNIEnv* env, jarray arr, native_type* ptr, std::size_t len) { 641 | static_assert(sizeof(native_type) == sizeof(java_type), "C++ boolean type and JNI jboolean type are expected to match in size."); 642 | env->GetBooleanArrayRegion(static_cast(arr), 0, len, reinterpret_cast(ptr)); 643 | } 644 | 645 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 646 | static_assert(sizeof(native_type) == sizeof(java_type), "C++ boolean type and JNI jboolean type are expected to match in size."); 647 | jbooleanArray arr = env->NewBooleanArray(len); 648 | if (arr == nullptr) { 649 | throw JavaException(env); 650 | } 651 | env->SetBooleanArrayRegion(arr, 0, len, reinterpret_cast(ptr)); 652 | return arr; 653 | } 654 | }; 655 | 656 | template 657 | struct CharArgType : FundamentalArgType { 658 | constexpr static std::string_view class_name = "java/lang/Byte"; 659 | constexpr static std::string_view primitive_type = "byte"; 660 | constexpr static std::string_view kotlin_type = "Byte"; 661 | constexpr static std::string_view type_sig = "B"; 662 | 663 | static jbyte java_call_method(JNIEnv* env, jobject obj, Method& m) { 664 | return env->CallByteMethod(obj, m.ref()); 665 | } 666 | 667 | static jbyte java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 668 | return env->GetByteField(obj, fld.ref()); 669 | } 670 | 671 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, T value) { 672 | env->SetByteField(obj, fld.ref(), java_value(env, value)); 673 | } 674 | 675 | static void native_array_value(JNIEnv* env, jarray arr, T* ptr, std::size_t len) { 676 | env->GetByteArrayRegion(static_cast(arr), 0, len, reinterpret_cast(ptr)); 677 | } 678 | 679 | static jarray java_array_value(JNIEnv* env, const T* ptr, std::size_t len) { 680 | jbyteArray arr = env->NewByteArray(len); 681 | if (arr == nullptr) { 682 | throw JavaException(env); 683 | } 684 | env->SetByteArrayRegion(arr, 0, len, reinterpret_cast(ptr)); 685 | return arr; 686 | } 687 | }; 688 | 689 | template <> struct ArgType : CharArgType {}; 690 | template <> struct ArgType : CharArgType {}; 691 | template <> struct ArgType : CharArgType {}; 692 | 693 | template <> 694 | struct ArgType : FundamentalArgType { 695 | constexpr static std::string_view class_name = "java/lang/Character"; 696 | constexpr static std::string_view primitive_type = "char"; 697 | constexpr static std::string_view kotlin_type = "Char"; 698 | constexpr static std::string_view type_sig = "C"; 699 | 700 | static jchar java_call_method(JNIEnv* env, jobject obj, Method& m) { 701 | return env->CallCharMethod(obj, m.ref()); 702 | } 703 | 704 | static jchar java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 705 | return env->GetCharField(obj, fld.ref()); 706 | } 707 | 708 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 709 | env->SetCharField(obj, fld.ref(), java_value(env, value)); 710 | } 711 | 712 | static void native_array_value(JNIEnv* env, jarray arr, native_type* ptr, std::size_t len) { 713 | env->GetCharArrayRegion(static_cast(arr), 0, len, ptr); 714 | } 715 | 716 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 717 | jcharArray arr = env->NewCharArray(len); 718 | if (arr == nullptr) { 719 | throw JavaException(env); 720 | } 721 | env->SetCharArrayRegion(arr, 0, len, ptr); 722 | return arr; 723 | } 724 | }; 725 | 726 | template <> 727 | struct ArgType : FundamentalArgType { 728 | constexpr static std::string_view class_name = "java/lang/Short"; 729 | constexpr static std::string_view primitive_type = "short"; 730 | constexpr static std::string_view kotlin_type = "Short"; 731 | constexpr static std::string_view type_sig = "S"; 732 | 733 | static jshort java_call_method(JNIEnv* env, jobject obj, Method& m) { 734 | return env->CallShortMethod(obj, m.ref()); 735 | } 736 | 737 | static jshort java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 738 | return env->GetShortField(obj, fld.ref()); 739 | } 740 | 741 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 742 | env->SetShortField(obj, fld.ref(), java_value(env, value)); 743 | } 744 | 745 | static void native_array_value(JNIEnv* env, jarray arr, native_type* ptr, std::size_t len) { 746 | env->GetShortArrayRegion(static_cast(arr), 0, len, ptr); 747 | } 748 | 749 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 750 | jshortArray arr = env->NewShortArray(len); 751 | if (arr == nullptr) { 752 | throw JavaException(env); 753 | } 754 | env->SetShortArrayRegion(arr, 0, len, ptr); 755 | return arr; 756 | } 757 | }; 758 | 759 | template 760 | struct Int32ArgType : FundamentalArgType { 761 | static_assert(sizeof(T) == 4, "32-bit integer type required."); 762 | 763 | constexpr static std::string_view class_name = "java/lang/Integer"; 764 | constexpr static std::string_view primitive_type = "int"; 765 | constexpr static std::string_view kotlin_type = "Int"; 766 | constexpr static std::string_view type_sig = "I"; 767 | 768 | static jint java_call_method(JNIEnv* env, jobject obj, Method& m) { 769 | return env->CallIntMethod(obj, m.ref()); 770 | } 771 | 772 | static jint java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 773 | return env->GetIntField(obj, fld.ref()); 774 | } 775 | 776 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, T value) { 777 | env->SetIntField(obj, fld.ref(), ArgType::java_value(env, value)); 778 | } 779 | 780 | static void native_array_value(JNIEnv* env, jarray arr, T* ptr, std::size_t len) { 781 | static_assert(sizeof(T) == sizeof(jint), "C++ and JNI integer types are expected to match in size."); 782 | env->GetIntArrayRegion(static_cast(arr), 0, len, reinterpret_cast(ptr)); 783 | } 784 | 785 | static jarray java_array_value(JNIEnv* env, const T* ptr, std::size_t len) { 786 | static_assert(sizeof(T) == sizeof(jint), "C++ and JNI integer types are expected to match in size."); 787 | jintArray arr = env->NewIntArray(len); 788 | if (arr == nullptr) { 789 | throw JavaException(env); 790 | } 791 | env->SetIntArrayRegion(arr, 0, len, reinterpret_cast(ptr)); 792 | return arr; 793 | } 794 | }; 795 | 796 | template 797 | struct Int64ArgType : FundamentalArgType { 798 | static_assert(sizeof(T) == 8, "64-bit integer type required."); 799 | 800 | constexpr static std::string_view class_name = "java/lang/Long"; 801 | constexpr static std::string_view primitive_type = "long"; 802 | constexpr static std::string_view kotlin_type = "Long"; 803 | constexpr static std::string_view type_sig = "J"; 804 | 805 | static jlong java_call_method(JNIEnv* env, jobject obj, Method& m) { 806 | return env->CallLongMethod(obj, m.ref()); 807 | } 808 | 809 | static jlong java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 810 | return env->GetLongField(obj, fld.ref()); 811 | } 812 | 813 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, T value) { 814 | env->SetLongField(obj, fld.ref(), ArgType::java_value(env, value)); 815 | } 816 | 817 | static void native_array_value(JNIEnv* env, jarray arr, T* ptr, std::size_t len) { 818 | static_assert(sizeof(T) == sizeof(jlong), "C++ and JNI integer types are expected to match in size."); 819 | env->GetLongArrayRegion(static_cast(arr), 0, len, reinterpret_cast(ptr)); 820 | } 821 | 822 | static jarray java_array_value(JNIEnv* env, const T* ptr, std::size_t len) { 823 | static_assert(sizeof(T) == sizeof(jlong), "C++ and JNI integer types are expected to match in size."); 824 | jlongArray arr = env->NewLongArray(len); 825 | if (arr == nullptr) { 826 | throw JavaException(env); 827 | } 828 | env->SetLongArrayRegion(arr, 0, len, reinterpret_cast(ptr)); 829 | return arr; 830 | } 831 | }; 832 | 833 | template 834 | struct IntegerArgType : std::conditional, Int64ArgType>::type {}; 835 | 836 | template <> struct ArgType : IntegerArgType {}; 837 | template <> struct ArgType : IntegerArgType {}; 838 | template <> struct ArgType : IntegerArgType {}; 839 | template <> struct ArgType : IntegerArgType {}; 840 | template <> struct ArgType : IntegerArgType {}; 841 | template <> struct ArgType : IntegerArgType {}; 842 | 843 | /** 844 | * Specialized type for storing native pointers in Java. 845 | * Reserved for use by the interoperability framework. 846 | */ 847 | template 848 | struct ArgType : private FundamentalArgType { 849 | constexpr static std::string_view class_name = "java/lang/Long"; 850 | constexpr static std::string_view kotlin_type = "Long"; 851 | constexpr static std::string_view type_sig = "J"; 852 | 853 | static T* native_field_value(JNIEnv* env, jobject obj, Field& fld) { 854 | return reinterpret_cast(native_value(env, env->GetLongField(obj, fld.ref()))); 855 | } 856 | 857 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, T* value) { 858 | env->SetLongField(obj, fld.ref(), java_value(env, reinterpret_cast(value))); 859 | } 860 | }; 861 | 862 | template <> 863 | struct ArgType : FundamentalArgType { 864 | constexpr static std::string_view class_name = "java/lang/Float"; 865 | constexpr static std::string_view primitive_type = "float"; 866 | constexpr static std::string_view kotlin_type = "Float"; 867 | constexpr static std::string_view type_sig = "F"; 868 | 869 | static jfloat java_call_method(JNIEnv* env, jobject obj, Method& m) { 870 | return env->CallFloatMethod(obj, m.ref()); 871 | } 872 | 873 | static jfloat java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 874 | return env->GetFloatField(obj, fld.ref()); 875 | } 876 | 877 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 878 | env->SetFloatField(obj, fld.ref(), java_value(env, value)); 879 | } 880 | 881 | static void native_array_value(JNIEnv* env, jarray arr, native_type* ptr, std::size_t len) { 882 | env->GetFloatArrayRegion(static_cast(arr), 0, len, ptr); 883 | } 884 | 885 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 886 | jfloatArray arr = env->NewFloatArray(len); 887 | if (arr == nullptr) { 888 | throw JavaException(env); 889 | } 890 | env->SetFloatArrayRegion(arr, 0, len, ptr); 891 | return arr; 892 | } 893 | }; 894 | 895 | template <> 896 | struct ArgType : FundamentalArgType { 897 | constexpr static std::string_view class_name = "java/lang/Double"; 898 | constexpr static std::string_view primitive_type = "double"; 899 | constexpr static std::string_view kotlin_type = "Double"; 900 | constexpr static std::string_view type_sig = "D"; 901 | 902 | static jdouble java_call_method(JNIEnv* env, jobject obj, Method& m) { 903 | return env->CallDoubleMethod(obj, m.ref()); 904 | } 905 | 906 | static jdouble java_raw_field_value(JNIEnv* env, jobject obj, Field& fld) { 907 | return env->GetDoubleField(obj, fld.ref()); 908 | } 909 | 910 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 911 | env->SetDoubleField(obj, fld.ref(), java_value(env, value)); 912 | } 913 | 914 | static void native_array_value(JNIEnv* env, jarray arr, native_type* ptr, std::size_t len) { 915 | env->GetDoubleArrayRegion(static_cast(arr), 0, len, ptr); 916 | } 917 | 918 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 919 | jdoubleArray arr = env->NewDoubleArray(len); 920 | if (arr == nullptr) { 921 | throw JavaException(env); 922 | } 923 | env->SetDoubleArrayRegion(arr, 0, len, ptr); 924 | return arr; 925 | } 926 | }; 927 | 928 | /** 929 | * Generates a human-readable name for specialized generic types, e.g. List. 930 | */ 931 | template 932 | struct kotlin_type_specialization { 933 | constexpr static std::string_view comma = ", "; 934 | constexpr static std::string_view lt = "<"; 935 | constexpr static std::string_view gt = ">"; 936 | constexpr static std::string_view value = join_v, gt>; 937 | }; 938 | 939 | /** 940 | * Reserved type tag to represent Java arrays. 941 | */ 942 | template 943 | struct Array; 944 | 945 | template 946 | struct ArgType> { 947 | private: 948 | constexpr static std::string_view array_type_prefix = "["; 949 | constexpr static std::string_view array_type = "Array"; 950 | 951 | public: 952 | constexpr static std::string_view kotlin_type = kotlin_type_specialization::kotlin_type>::value; 953 | constexpr static std::string_view type_sig = join_v::type_sig>; 954 | using native_type = Array; 955 | using java_type = jarray; 956 | }; 957 | 958 | /** 959 | * Base class for all object types such as String, List, Map, data classes and native classes. 960 | */ 961 | template 962 | struct CompositeArgType { 963 | using native_type = T; 964 | using java_type = J; 965 | 966 | private: 967 | constexpr static std::string_view class_type_prefix = "L"; 968 | constexpr static std::string_view class_type_suffix = ";"; 969 | 970 | public: 971 | constexpr static std::string_view kotlin_type = ArgType::qualified_name; 972 | constexpr static std::string_view type_sig = join_v::class_name, class_type_suffix>; 973 | 974 | static native_type native_field_value(JNIEnv* env, jobject obj, Field& fld) { 975 | return ArgType::native_value(env, env->GetObjectField(obj, fld.ref())); 976 | } 977 | 978 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, native_type value) { 979 | // use local reference to ensure temporary object is released 980 | LocalObjectRef objFieldValue(env, ArgType::java_value(env, value)); 981 | env->SetObjectField(obj, fld.ref(), objFieldValue.ref()); 982 | } 983 | 984 | static jobject java_box(JNIEnv* env, J value) { 985 | return value; 986 | } 987 | 988 | static J java_unbox(JNIEnv* env, jobject obj) { 989 | return obj; 990 | } 991 | }; 992 | 993 | /** 994 | * Reserved type tag to represent Java objects. 995 | */ 996 | struct Object; 997 | 998 | template <> 999 | struct ArgType : CompositeArgType { 1000 | constexpr static std::string_view qualified_name = "java.lang.Object"; 1001 | constexpr static std::string_view class_name = "java/lang/Object"; 1002 | }; 1003 | 1004 | /** 1005 | * Converts a C++ string (represented in UTF-8) into a java.lang.String. 1006 | */ 1007 | template <> 1008 | struct ArgType : CompositeArgType { 1009 | constexpr static std::string_view qualified_name = "java.lang.String"; 1010 | constexpr static std::string_view class_name = "java/lang/String"; 1011 | 1012 | static jstring java_unbox(JNIEnv* env, jobject obj) { 1013 | return static_cast(obj); // only a pointer cast 1014 | } 1015 | 1016 | static native_type native_field_value(JNIEnv* env, jobject obj, Field& fld) { 1017 | return native_value(env, static_cast(env->GetObjectField(obj, fld.ref()))); 1018 | } 1019 | 1020 | static std::string native_value(JNIEnv* env, jstring value) { 1021 | jsize len = env->GetStringUTFLength(value); 1022 | std::string s; 1023 | if (len > 0) { 1024 | const char* chars = env->GetStringUTFChars(value, nullptr); 1025 | s.assign(chars, len); 1026 | env->ReleaseStringUTFChars(value, chars); 1027 | } 1028 | return s; 1029 | } 1030 | 1031 | static jstring java_value(JNIEnv* env, const std::string& value) { 1032 | return env->NewStringUTF(value.data()); 1033 | } 1034 | }; 1035 | 1036 | /** 1037 | * Converts a C++ collection with a forward iterator into a Java List. 1038 | */ 1039 | template 1040 | struct ListArgType : CompositeArgType { 1041 | constexpr static std::string_view qualified_name = "java.util.List"; 1042 | constexpr static std::string_view class_name = "java/util/List"; 1043 | constexpr static std::string_view kotlin_type = kotlin_type_specialization::kotlin_type>::value; 1044 | 1045 | public: 1046 | static L native_value(JNIEnv* env, jobject list); 1047 | static jobject java_value(JNIEnv* env, const L& value); 1048 | }; 1049 | 1050 | template 1051 | struct FundamentalArgArrayType { 1052 | using native_type = std::vector; 1053 | using java_type = jarray; 1054 | 1055 | private: 1056 | constexpr static std::string_view kotlin_array = "Array"; 1057 | constexpr static std::string_view array_type_prefix = "["; 1058 | 1059 | public: 1060 | constexpr static std::string_view kotlin_type = join_v::kotlin_type, kotlin_array>; 1061 | constexpr static std::string_view type_sig = join_v::type_sig>; 1062 | 1063 | static native_type native_field_value(JNIEnv* env, jobject obj, Field& fld) { 1064 | LocalObjectRef objFieldValue(env, env->GetObjectField(obj, fld.ref())); 1065 | return native_value(env, static_cast(objFieldValue.ref())); 1066 | } 1067 | 1068 | static void java_field_value(JNIEnv* env, jobject obj, Field& fld, const native_type& value) { 1069 | LocalObjectRef objFieldValue(env, java_value(env, value)); 1070 | env->SetObjectField(obj, fld.ref(), objFieldValue.ref()); 1071 | } 1072 | 1073 | static jarray java_box(JNIEnv* env, jarray value) { 1074 | return value; 1075 | } 1076 | 1077 | static jarray java_unbox(JNIEnv* env, jarray obj) { 1078 | return obj; 1079 | } 1080 | 1081 | static native_type native_value(JNIEnv* env, jarray arr) { 1082 | std::size_t len = env->GetArrayLength(arr); 1083 | native_type vec(len); 1084 | ArgType::native_array_value(env, arr, vec.data(), vec.size()); 1085 | return vec; 1086 | } 1087 | 1088 | static jarray java_value(JNIEnv* env, const native_type& arr) { 1089 | return ArgType::java_array_value(env, arr.data(), arr.size()); 1090 | } 1091 | }; 1092 | 1093 | /** 1094 | * Converts a native dynamically-sized array into a Java List. 1095 | */ 1096 | template 1097 | struct ArgType> : ListArgType, T> {}; 1098 | 1099 | template <> struct ArgType> : FundamentalArgArrayType {}; 1100 | template <> struct ArgType> : FundamentalArgArrayType {}; 1101 | template <> struct ArgType> : FundamentalArgArrayType {}; 1102 | template <> struct ArgType> : FundamentalArgArrayType {}; 1103 | template <> struct ArgType> : FundamentalArgArrayType {}; 1104 | template <> struct ArgType> : FundamentalArgArrayType {}; 1105 | template <> struct ArgType> : FundamentalArgArrayType {}; 1106 | template <> struct ArgType> : FundamentalArgArrayType {}; 1107 | template <> struct ArgType> : FundamentalArgArrayType {}; 1108 | template <> struct ArgType> : FundamentalArgArrayType {}; 1109 | template <> struct ArgType> : FundamentalArgArrayType {}; 1110 | template <> struct ArgType> : FundamentalArgArrayType {}; 1111 | template <> struct ArgType> : FundamentalArgArrayType {}; 1112 | 1113 | template 1114 | struct ArgType> : ListArgType, T> {}; 1115 | 1116 | /** 1117 | * Converts a native set (e.g. a [set] or [unordered_set]) into a Java Set. 1118 | */ 1119 | template 1120 | struct SetArgType : CompositeArgType { 1121 | constexpr static std::string_view qualified_name = "java.util.Set"; 1122 | constexpr static std::string_view class_name = "java/util/Set"; 1123 | constexpr static std::string_view kotlin_type = kotlin_type_specialization::kotlin_type>::value; 1124 | 1125 | static S native_value(JNIEnv* env, jobject set); 1126 | static jobject java_value(JNIEnv* env, const S& nativeSet); 1127 | }; 1128 | 1129 | template 1130 | struct ArgType> : SetArgType, E> { 1131 | constexpr static std::string_view concrete_class_name = "java/util/HashSet"; 1132 | }; 1133 | 1134 | template 1135 | struct ArgType> : SetArgType, E> { 1136 | constexpr static std::string_view concrete_class_name = "java/util/TreeSet"; 1137 | }; 1138 | 1139 | /** 1140 | * Converts a native dictionary (e.g. a [map] or [unordered_map]) into a Java Map. 1141 | */ 1142 | template 1143 | struct MapArgType : CompositeArgType { 1144 | constexpr static std::string_view qualified_name = "java.util.Map"; 1145 | constexpr static std::string_view class_name = "java/util/Map"; 1146 | constexpr static std::string_view kotlin_type = kotlin_type_specialization::kotlin_type, ArgType::kotlin_type>::value; 1147 | 1148 | static M native_value(JNIEnv* env, jobject map); 1149 | static jobject java_value(JNIEnv* env, const M& nativeMap); 1150 | }; 1151 | 1152 | template 1153 | struct ArgType> : MapArgType, K, V> { 1154 | constexpr static std::string_view concrete_class_name = "java/util/HashMap"; 1155 | }; 1156 | 1157 | template 1158 | struct ArgType> : MapArgType, K, V> { 1159 | constexpr static std::string_view concrete_class_name = "java/util/TreeMap"; 1160 | }; 1161 | 1162 | /** 1163 | * Meta-information about a native class member variable. 1164 | */ 1165 | struct FieldBinding { 1166 | /** The field name as it appears in the class definition. */ 1167 | std::string_view name; 1168 | /** The Java type signature associated with the field */ 1169 | std::string_view signature; 1170 | /** A function that extracts a value from a Java object field. */ 1171 | void (*get_by_value)(JNIEnv* env, jobject obj, Field& fld, const void* native_object_ptr); 1172 | /** A function that persists a value to a Java object field. */ 1173 | void (*set_by_value)(JNIEnv* env, jobject obj, Field& fld, void* native_object_ptr); 1174 | }; 1175 | 1176 | /** 1177 | * Stores meta-information about the member variables a native class type has. 1178 | */ 1179 | struct FieldBindings { 1180 | inline static std::map< std::string_view, std::vector > value; 1181 | }; 1182 | 1183 | /** 1184 | * Marshals types that are passed by value between C++ and Java/Kotlin. 1185 | */ 1186 | template 1187 | struct DataClassArgType : CompositeArgType { 1188 | using native_type = T; 1189 | 1190 | constexpr static std::string_view type_sig = CompositeArgType::type_sig; 1191 | 1192 | static T native_value(JNIEnv* env, jobject obj) { 1193 | LocalClassRef objClass(env, obj); 1194 | 1195 | T native_object; 1196 | auto&& bindings = FieldBindings::value[type_sig]; 1197 | for (auto&& binding : bindings) { 1198 | Field fld = objClass.getField(binding.name.data(), binding.signature); 1199 | binding.set_by_value(env, obj, fld, &native_object); 1200 | } 1201 | return native_object; 1202 | } 1203 | 1204 | static jobject java_value(JNIEnv* env, const T& native_object) { 1205 | LocalClassRef objClass(env, type_sig.data()); 1206 | jobject obj = env->AllocObject(objClass.ref()); 1207 | if (obj == nullptr) { 1208 | throw JavaException(env); 1209 | } 1210 | 1211 | auto&& bindings = FieldBindings::value[type_sig]; 1212 | for (auto&& binding : bindings) { 1213 | Field fld = objClass.getField(binding.name.data(), binding.signature); 1214 | binding.get_by_value(env, obj, fld, &native_object); 1215 | } 1216 | return obj; 1217 | } 1218 | 1219 | static jarray java_array_value(JNIEnv* env, const native_type* ptr, std::size_t len) { 1220 | LocalClassRef elementClass(env, ArgType::class_name.data()); 1221 | jobjectArray arr = env->NewObjectArray(len, elementClass.ref(), nullptr); 1222 | if (arr == nullptr) { 1223 | throw JavaException(env); 1224 | } 1225 | for (std::size_t k = 0; k < len; ++k) { 1226 | LocalObjectRef objElement(env, ArgType::java_value(env, ptr[k])); 1227 | env->SetObjectArrayElement(arr, k, objElement.ref()); 1228 | } 1229 | return arr; 1230 | } 1231 | }; 1232 | 1233 | /** 1234 | * Marshals types that live primarily in the native space, and accessed through an opaque handle in Java/Kotlin. 1235 | */ 1236 | template 1237 | struct NativeClassArgType : CompositeArgType { 1238 | using native_type = T; 1239 | 1240 | constexpr static std::string_view type_sig = CompositeArgType::type_sig; 1241 | 1242 | static T& native_value(JNIEnv* env, jobject obj) { 1243 | // look up field that stores native pointer 1244 | LocalClassRef cls(env, obj); 1245 | Field field = cls.getField("nativePointer", ArgType::type_sig.data()); 1246 | T* ptr = ArgType::native_field_value(env, obj, field); 1247 | return *ptr; 1248 | } 1249 | 1250 | static jobject java_value(JNIEnv* env, T&& native_object) { 1251 | // instantiate native object using copy or move constructor 1252 | T* ptr = new T(std::forward(native_object)); 1253 | 1254 | // instantiate Java object by skipping constructor 1255 | LocalClassRef objClass(env, ArgType::class_name.data()); 1256 | jobject obj = env->AllocObject(objClass.ref()); 1257 | if (obj == nullptr) { 1258 | throw JavaException(env); 1259 | } 1260 | 1261 | // store native pointer in Java object field 1262 | Field field = objClass.getField("nativePointer", ArgType::type_sig.data()); 1263 | ArgType::java_field_value(env, obj, field, ptr); 1264 | 1265 | return obj; 1266 | } 1267 | }; 1268 | 1269 | template 1270 | struct FieldType; 1271 | 1272 | template 1273 | struct FieldType { 1274 | using type = R; 1275 | }; 1276 | 1277 | template 1278 | struct Function; 1279 | 1280 | /** 1281 | * Extracts a Java signature from a native callable. 1282 | */ 1283 | template 1284 | struct Function { 1285 | private: 1286 | static_assert(std::is_same::native_type>::value, "Unrecognized type detected, ensure that all involved types have been declared as a binding type with DECLARE_DATA_CLASS or DECLARE_NATIVE_CLASS."); 1287 | static_assert((std::is_same::native_type>::value && ...), "Unrecognized type detected, ensure that all involved types have been declared as a binding type with DECLARE_DATA_CLASS or DECLARE_NATIVE_CLASS."); 1288 | 1289 | template 1290 | struct callable_sig { 1291 | constexpr static std::string_view lparen = "("; 1292 | constexpr static std::string_view rparen = ")"; 1293 | constexpr static std::string_view value = join_v; 1294 | }; 1295 | 1296 | template 1297 | struct function_params; 1298 | 1299 | template 1300 | struct function_params> { 1301 | constexpr static std::string_view arg = "arg"; 1302 | constexpr static std::string_view comma = ", "; 1303 | constexpr static std::string_view colon = ": "; 1304 | constexpr static std::string_view value = join_sep_v::value, colon, ArgType::kotlin_type>...>; 1305 | }; 1306 | 1307 | struct lambda_params { 1308 | constexpr static std::string_view comma = ", "; 1309 | constexpr static std::string_view value = join_sep_v::kotlin_type...>; 1310 | }; 1311 | 1312 | constexpr static std::string_view param_sig = join_v::type_sig...>; 1313 | constexpr static std::string_view return_sig = ArgType::type_sig; 1314 | 1315 | constexpr static std::string_view colon = ": "; 1316 | constexpr static std::string_view kotlin_params = function_params>::value; 1317 | constexpr static std::string_view kotlin_return = join_v::kotlin_type>; 1318 | 1319 | constexpr static std::string_view arrow = " -> "; 1320 | constexpr static std::string_view kotlin_lambda_params = lambda_params::value; 1321 | constexpr static std::string_view kotlin_lambda_return = join_v::kotlin_type>; 1322 | 1323 | public: 1324 | /** Java signature string used internally for type lookup. */ 1325 | constexpr static std::string_view signature = callable_sig::value; 1326 | 1327 | /** Human-readable type definition for use as a member function. */ 1328 | constexpr static std::string_view kotlin_type = callable_sig::value; 1329 | 1330 | /** Human-readable type definition for use as a lambda type. */ 1331 | constexpr static std::string_view kotlin_lambda_type = callable_sig::value; 1332 | }; 1333 | 1334 | /** 1335 | * Extracts a Java signature from a native free (non-member) function. 1336 | */ 1337 | template 1338 | struct Function { 1339 | constexpr static std::string_view signature = Function...)>::signature; 1340 | constexpr static std::string_view kotlin_type = Function...)>::kotlin_type; 1341 | }; 1342 | 1343 | /** 1344 | * Extracts a Java signature from a native member function. 1345 | */ 1346 | template 1347 | struct Function { 1348 | constexpr static std::string_view signature = Function...)>::signature; 1349 | constexpr static std::string_view kotlin_type = Function...)>::kotlin_type; 1350 | }; 1351 | 1352 | /** 1353 | * Extracts a Java signature from a const-qualified native member function. 1354 | */ 1355 | template 1356 | struct Function { 1357 | constexpr static std::string_view signature = Function...)>::signature; 1358 | constexpr static std::string_view kotlin_type = Function...)>::kotlin_type; 1359 | }; 1360 | 1361 | /** 1362 | * Acts as a Java/Kotlin callback proxy in C++ code or a C++ function object proxy in Java/Kotlin code. 1363 | */ 1364 | template 1365 | struct ArgType> { 1366 | using native_type = std::function; 1367 | using java_type = jobject; 1368 | 1369 | template 1370 | struct as_object { 1371 | using type = Object; 1372 | }; 1373 | 1374 | constexpr static std::string_view kotlin_function_type = "Lkotlin/jvm/functions/Function"; 1375 | constexpr static std::string_view semicolon = ";"; 1376 | constexpr static std::string_view type_sig = join_v::value, semicolon>; 1377 | constexpr static std::string_view kotlin_type = Function::kotlin_lambda_type; 1378 | 1379 | private: 1380 | constexpr static std::string_view invoke_sig = Function::type...)>::signature; 1381 | 1382 | public: 1383 | static std::function native_value(JNIEnv* env, jobject value) { 1384 | GlobalObjectRef fun = GlobalObjectRef(env, value); 1385 | LocalClassRef cls(env, fun.ref()); 1386 | Method invoke = cls.getMethod("invoke", invoke_sig); // lifecycle bound to object reference 1387 | return [fun = std::move(fun), invoke = std::move(invoke)](Args... args) -> R { 1388 | // retrieve an environment reference (which may not be the same as when the function object was created) 1389 | JNIEnv* env = this_thread.getEnv(); 1390 | if (!env) { 1391 | assert(!"consistency failure"); 1392 | if constexpr (!std::is_same_v) { 1393 | return R(); 1394 | } else { 1395 | return; 1396 | } 1397 | } 1398 | 1399 | // Kotlin's `FunctionX` family of classes have an `invoke` method that takes and returns Object instances; 1400 | // primitive types need boxing/unboxing 1401 | if constexpr (!std::is_same_v) { 1402 | auto objResult = LocalObjectRef(env, 1403 | env->CallObjectMethod( 1404 | fun.ref(), invoke.ref(), 1405 | LocalObjectRef(env, 1406 | ArgType::java_box(env, ArgType::java_value(env, args)) 1407 | ).ref()... 1408 | ) 1409 | ); 1410 | if (env->ExceptionCheck()) { 1411 | throw JavaException(env); 1412 | } 1413 | return ArgType::native_value(env, ArgType::java_unbox(env, objResult.ref())); 1414 | } else { 1415 | env->CallVoidMethod( 1416 | fun.ref(), invoke.ref(), 1417 | LocalObjectRef(env, 1418 | ArgType::java_box(env, ArgType::java_value(env, args)) 1419 | ).ref()... 1420 | ); 1421 | if (env->ExceptionCheck()) { 1422 | throw JavaException(env); 1423 | } 1424 | } 1425 | }; 1426 | } 1427 | 1428 | static jobject java_value(JNIEnv* env, const std::function& value) { 1429 | throw std::runtime_error("C++ functions returning a function object to Java/Kotlin are not supported."); 1430 | } 1431 | }; 1432 | 1433 | template 1434 | using java_t = typename ArgType::java_type; 1435 | 1436 | template 1437 | L ListArgType::native_value(JNIEnv* env, jobject list) { 1438 | LocalClassRef listClass(env, list); 1439 | 1440 | Method sizeFunc = listClass.getMethod("size", Function::signature); 1441 | jint len = env->CallIntMethod(list, sizeFunc.ref()); 1442 | 1443 | Method getFunc = listClass.getMethod("get", Function::signature); 1444 | 1445 | L nativeList; 1446 | for (jint i = 0; i < len; i++) { 1447 | LocalObjectRef listElement(env, env->CallObjectMethod(list, getFunc.ref(), i)); 1448 | nativeList.push_back(ArgType::native_value(env, ArgType::java_unbox(env, listElement.ref()))); 1449 | } 1450 | 1451 | return nativeList; 1452 | } 1453 | 1454 | template 1455 | jobject ListArgType::java_value(JNIEnv* env, const L& nativeList) { 1456 | LocalClassRef arrayListClass(env, "java/util/ArrayList"); 1457 | Method initFunc = arrayListClass.getMethod("", Function::signature); 1458 | jobject arrayList = env->NewObject(arrayListClass.ref(), initFunc.ref(), nativeList.size()); 1459 | Method addFunc = arrayListClass.getMethod("add", Function::signature); 1460 | 1461 | for (auto&& element : nativeList) { 1462 | LocalObjectRef arrayListElement(env, ArgType::java_box(env, ArgType::java_value(env, element))); 1463 | env->CallBooleanMethod(arrayList, addFunc.ref(), arrayListElement.ref()); 1464 | } 1465 | return arrayList; 1466 | } 1467 | 1468 | template 1469 | S SetArgType::native_value(JNIEnv* env, jobject set) { 1470 | LocalClassRef setClass(env, set); 1471 | Method iteratorFunc = setClass.getMethod("iterator", "()Ljava/util/Iterator;"); 1472 | 1473 | LocalClassRef iteratorClass(env, "java/util/Iterator"); 1474 | Method hasNextFunc = iteratorClass.getMethod("hasNext", "()Z"); 1475 | Method nextFunc = iteratorClass.getMethod("next", "()Ljava/lang/Object;"); 1476 | 1477 | LocalObjectRef setIterator(env, env->CallObjectMethod(set, iteratorFunc.ref())); 1478 | 1479 | S nativeSet; 1480 | bool hasNext = static_cast(env->CallBooleanMethod(setIterator.ref(), hasNextFunc.ref())); 1481 | while (hasNext) { 1482 | LocalObjectRef setElement(env, env->CallObjectMethod(setIterator.ref(), nextFunc.ref())); 1483 | auto&& element = ArgType::java_unbox(env, setElement.ref()); 1484 | 1485 | nativeSet.insert(ArgType::native_value(env, element)); 1486 | 1487 | hasNext = static_cast(env->CallBooleanMethod(setIterator.ref(), hasNextFunc.ref())); 1488 | } 1489 | return nativeSet; 1490 | } 1491 | 1492 | template 1493 | jobject SetArgType::java_value(JNIEnv* env, const S& nativeSet) { 1494 | LocalClassRef setClass(env, ArgType::concrete_class_name.data()); 1495 | Method initFunc = setClass.getMethod("", Function::signature); 1496 | Method addFunc = setClass.getMethod("add", Function::signature); 1497 | jobject set = env->NewObject(setClass.ref(), initFunc.ref()); 1498 | if (set == nullptr) { 1499 | throw JavaException(env); 1500 | } 1501 | 1502 | for (auto&& item : nativeSet) { 1503 | LocalObjectRef element(env, ArgType::java_box(env, ArgType::java_value(env, item))); 1504 | env->CallBooleanMethod(set, addFunc.ref(), element.ref()); 1505 | } 1506 | return set; 1507 | } 1508 | 1509 | template 1510 | M MapArgType::native_value(JNIEnv* env, jobject map) { 1511 | LocalClassRef mapClass(env, map); 1512 | Method entrySetFunc = mapClass.getMethod("entrySet", "()Ljava/util/Set;"); 1513 | 1514 | LocalClassRef entrySetClass(env, "java/util/Set"); 1515 | Method iteratorFunc = entrySetClass.getMethod("iterator", "()Ljava/util/Iterator;"); 1516 | 1517 | LocalClassRef iteratorClass(env, "java/util/Iterator"); 1518 | Method hasNextFunc = iteratorClass.getMethod("hasNext", "()Z"); 1519 | Method nextFunc = iteratorClass.getMethod("next", "()Ljava/lang/Object;"); 1520 | 1521 | LocalClassRef entryClass(env, "java/util/Map$Entry"); 1522 | Method getKeyFunc = entryClass.getMethod("getKey", "()Ljava/lang/Object;"); 1523 | Method getValueFunc = entryClass.getMethod("getValue", "()Ljava/lang/Object;"); 1524 | 1525 | LocalObjectRef mapEntrySet(env, env->CallObjectMethod(map, entrySetFunc.ref())); 1526 | LocalObjectRef mapIterator(env, env->CallObjectMethod(mapEntrySet.ref(), iteratorFunc.ref())); 1527 | 1528 | M nativeMap; 1529 | bool hasNext = static_cast(env->CallBooleanMethod(mapIterator.ref(), hasNextFunc.ref())); 1530 | while (hasNext) { 1531 | LocalObjectRef mapEntry(env, env->CallObjectMethod(mapIterator.ref(), nextFunc.ref())); 1532 | LocalObjectRef mapKey(env, env->CallObjectMethod(mapEntry.ref(), getKeyFunc.ref())); 1533 | LocalObjectRef mapValue(env, env->CallObjectMethod(mapEntry.ref(), getValueFunc.ref())); 1534 | 1535 | auto&& key = ArgType::java_unbox(env, mapKey.ref()); 1536 | auto&& value = ArgType::java_unbox(env, mapValue.ref()); 1537 | 1538 | nativeMap[ArgType::native_value(env, key)] = ArgType::native_value(env, value); 1539 | 1540 | hasNext = static_cast(env->CallBooleanMethod(mapIterator.ref(), hasNextFunc.ref())); 1541 | } 1542 | 1543 | return nativeMap; 1544 | } 1545 | 1546 | template 1547 | jobject MapArgType::java_value(JNIEnv* env, const M& nativeMap) { 1548 | LocalClassRef mapClass(env, ArgType::concrete_class_name.data()); 1549 | Method initFunc = mapClass.getMethod("", Function::signature); 1550 | Method putFunc = mapClass.getMethod("put", Function::signature); 1551 | jobject map = env->NewObject(mapClass.ref(), initFunc.ref(), nativeMap.size()); 1552 | if (map == nullptr) { 1553 | throw JavaException(env); 1554 | } 1555 | 1556 | for (auto&& item : nativeMap) { 1557 | LocalObjectRef key(env, ArgType::java_box(env, ArgType::java_value(env, item.first))); 1558 | LocalObjectRef value(env, ArgType::java_box(env, ArgType::java_value(env, item.second))); 1559 | env->CallObjectMethod(map, putFunc.ref(), key.ref(), value.ref()); 1560 | } 1561 | return map; 1562 | } 1563 | 1564 | template 1565 | struct types { 1566 | using type = types; 1567 | }; 1568 | 1569 | /** 1570 | * Gets a list of call argument types from a function-like type signature. 1571 | */ 1572 | template 1573 | struct args; 1574 | 1575 | template 1576 | struct args : types {}; 1577 | 1578 | template 1579 | struct args : args {}; 1580 | 1581 | template 1582 | struct args : args {}; 1583 | 1584 | template 1585 | struct args : args {}; 1586 | 1587 | template 1588 | using args_t = typename args::type; 1589 | 1590 | template 1591 | struct is_free_function_pointer 1592 | : std::integral_constant && std::is_function_v>> 1593 | {}; 1594 | 1595 | struct JavaOutputBuffer : std::stringbuf { 1596 | JavaOutputBuffer(JNIEnv* env) 1597 | : _env(env) 1598 | , _out(LocalClassRef(env, "java/lang/System").getStaticObjectField("out", "Ljava/io/PrintStream;")) 1599 | , _print(LocalClassRef(env, "java/io/PrintStream").getMethod("print", "(Ljava/lang/String;)V")) 1600 | {} 1601 | 1602 | virtual int sync() { 1603 | _env->CallVoidMethod(_out.ref(), _print.ref(), LocalObjectRef(_env, ArgType::java_value(_env, str())).ref()); 1604 | str(""); 1605 | return 0; 1606 | } 1607 | 1608 | private: 1609 | JNIEnv* _env; 1610 | LocalObjectRef _out; 1611 | Method _print; 1612 | }; 1613 | 1614 | /** 1615 | * Prints to the Java standard output `System.out`. 1616 | */ 1617 | struct JavaOutput { 1618 | JavaOutput(JNIEnv* env) : _buf(env), _str(&_buf) {} 1619 | 1620 | ~JavaOutput() { 1621 | _buf.pubsync(); 1622 | } 1623 | 1624 | std::ostream& stream() { 1625 | return _str; 1626 | } 1627 | 1628 | private: 1629 | JavaOutputBuffer _buf; 1630 | std::ostream _str; 1631 | }; 1632 | 1633 | /** 1634 | * Converts a native exception into a Java exception. 1635 | */ 1636 | inline void exception_handler(JNIEnv* env, std::exception& ex) { 1637 | // ensure that no unhandled Java exception is waiting to be thrown 1638 | if (!env->ExceptionCheck()) { 1639 | LocalClassRef cls(env, "java/lang/Exception"); 1640 | env->ThrowNew(cls.ref(), ex.what()); 1641 | } 1642 | } 1643 | 1644 | /** 1645 | * Wraps a native function pointer into a function pointer callable from Java. 1646 | * Adapts a function with the signature R(*func)(Args...). 1647 | * @tparam func The callable function pointer. 1648 | * @return A type-safe function pointer to pass to Java's [RegisterNatives] function. 1649 | */ 1650 | template 1651 | struct Adapter { 1652 | using result_type = decltype(func(std::declval()...)); 1653 | 1654 | static java_t invoke(JNIEnv* env, jclass obj, java_t>... args) { 1655 | try { 1656 | if constexpr (!std::is_same_v) { 1657 | auto&& result = func(ArgType>::native_value(env, args)...); 1658 | return ArgType::java_value(env, std::move(result)); 1659 | } else { 1660 | func(ArgType>::native_value(env, args)...); 1661 | } 1662 | } catch (JavaException& ex) { 1663 | env->Throw(ex.innerException()); 1664 | if constexpr (!std::is_same_v) { 1665 | return java_t(); 1666 | } 1667 | } catch (std::exception& ex) { 1668 | exception_handler(env, ex); 1669 | if constexpr (!std::is_same_v) { 1670 | return java_t(); 1671 | } 1672 | } 1673 | } 1674 | }; 1675 | 1676 | /** 1677 | * Wraps a native member function pointer into a function pointer callable from Java. 1678 | * Adapts a function with the signature R(T::*func)(Args...). 1679 | * @tparam func The callable member function pointer. 1680 | * @return A type-safe function pointer to pass to Java's [RegisterNatives] function. 1681 | */ 1682 | template 1683 | struct MemberAdapter { 1684 | using result_type = decltype((std::declval().*func)(std::declval()...)); 1685 | 1686 | static java_t invoke(JNIEnv* env, jobject obj, java_t>... args) { 1687 | try { 1688 | // look up field that stores native pointer 1689 | LocalClassRef cls(env, obj); 1690 | Field field = cls.getField("nativePointer", ArgType::type_sig.data()); 1691 | T* ptr = ArgType::native_field_value(env, obj, field); 1692 | 1693 | // invoke native function 1694 | if (!ptr) { 1695 | throw std::logic_error(msg() << "Object " << ArgType::class_name << " has already been disposed of."); 1696 | } 1697 | if constexpr (!std::is_same_v) { 1698 | auto&& result = (ptr->*func)(ArgType>::native_value(env, args)...); 1699 | return ArgType::java_value(env, std::move(result)); 1700 | } else { 1701 | (ptr->*func)(ArgType>::native_value(env, args)...); 1702 | } 1703 | 1704 | } catch (JavaException& ex) { 1705 | env->Throw(ex.innerException()); 1706 | if constexpr (!std::is_same_v) { 1707 | return java_t(); 1708 | } 1709 | } catch (std::exception& ex) { 1710 | exception_handler(env, ex); 1711 | if constexpr (!std::is_same_v) { 1712 | return java_t(); 1713 | } 1714 | } 1715 | } 1716 | }; 1717 | 1718 | /** 1719 | * Wraps a native function pointer or a member function pointer into a function pointer callable from Java. 1720 | * @tparam func The callable function or member function pointer. 1721 | * @return A type-erased function pointer to pass to Java's [RegisterNatives] function. 1722 | */ 1723 | template 1724 | constexpr void* callable(types) { 1725 | auto&& f = std::conditional_t< 1726 | std::is_member_function_pointer_v, 1727 | MemberAdapter, 1728 | Adapter 1729 | >::invoke; 1730 | return reinterpret_cast(f); 1731 | } 1732 | 1733 | /** 1734 | * Adapts a constructor function to be invoked from Java on object instantiation with a class method. 1735 | */ 1736 | template 1737 | struct CreateObjectAdapter { 1738 | static jobject invoke(JNIEnv* env, jclass cls, java_t... args) { 1739 | try { 1740 | // instantiate native object 1741 | T* ptr = new T(ArgType::native_value(env, args)...); 1742 | 1743 | // instantiate Java object by skipping constructor 1744 | LocalClassRef objClass(env, cls); 1745 | jobject obj = env->AllocObject(objClass.ref()); 1746 | if (obj == nullptr) { 1747 | throw JavaException(env); 1748 | } 1749 | 1750 | // store native pointer in Java object field 1751 | Field field = objClass.getField("nativePointer", ArgType::type_sig.data()); 1752 | ArgType::java_field_value(env, obj, field, ptr); 1753 | 1754 | return obj; 1755 | } catch (JavaException& ex) { 1756 | env->Throw(ex.innerException()); 1757 | return nullptr; 1758 | } catch (std::exception& ex) { 1759 | exception_handler(env, ex); 1760 | return nullptr; 1761 | } 1762 | } 1763 | }; 1764 | 1765 | /** 1766 | * Adapts a destructor function to be invoked from Java when the object is being disposed of. 1767 | * This function is bound to the method close() inherited from the interface AutoCloseable. 1768 | */ 1769 | template 1770 | struct DestroyObjectAdapter { 1771 | static void invoke(JNIEnv* env, jobject obj) { 1772 | try { 1773 | // look up field that stores native pointer 1774 | LocalClassRef cls(env, obj); 1775 | Field field = cls.getField("nativePointer", ArgType::type_sig.data()); 1776 | T* ptr = ArgType::native_field_value(env, obj, field); 1777 | 1778 | // release native object 1779 | delete ptr; 1780 | 1781 | // prevent accidental duplicate delete 1782 | ArgType::java_field_value(env, obj, field, nullptr); 1783 | } catch (JavaException& ex) { 1784 | env->Throw(ex.innerException()); 1785 | } catch (std::exception& ex) { 1786 | exception_handler(env, ex); 1787 | } 1788 | } 1789 | }; 1790 | 1791 | template 1792 | constexpr void* object_initialization(types) { 1793 | return reinterpret_cast(CreateObjectAdapter::invoke); 1794 | } 1795 | 1796 | template 1797 | constexpr void* object_termination() { 1798 | return reinterpret_cast(DestroyObjectAdapter::invoke); 1799 | } 1800 | 1801 | struct FunctionBinding { 1802 | std::string_view name; 1803 | std::string_view signature; 1804 | bool is_member; 1805 | void* function_entry_point; 1806 | std::string_view friendly_signature; 1807 | }; 1808 | 1809 | struct FunctionBindings { 1810 | inline static std::map< std::string_view, std::vector > value; 1811 | }; 1812 | 1813 | /** 1814 | * Represents a native class in Java. 1815 | * The Java object holds an opaque pointer to the native object. The lifecycle of the object is governed by Java. 1816 | */ 1817 | template 1818 | struct native_class { 1819 | native_class() { 1820 | auto&& bindings = FunctionBindings::value[ArgType::class_name]; 1821 | bindings.push_back({ 1822 | "close", 1823 | Function::signature, 1824 | true, 1825 | object_termination(), 1826 | Function::kotlin_type 1827 | }); 1828 | } 1829 | 1830 | native_class(const native_class&) = delete; 1831 | native_class(native_class&&) = delete; 1832 | 1833 | template 1834 | native_class& constructor(std::string_view name) { 1835 | static_assert(std::is_function_v, "Use a function signature such as Sample(int, std::string) to identify a constructor."); 1836 | 1837 | auto&& bindings = FunctionBindings::value[ArgType::class_name]; 1838 | bindings.push_back({ 1839 | name, 1840 | Function::signature, 1841 | false, 1842 | object_initialization(args_t{}), 1843 | Function::kotlin_type 1844 | }); 1845 | return *this; 1846 | } 1847 | 1848 | template 1849 | native_class& function(const char* name) { 1850 | using func_type = decltype(func); 1851 | 1852 | // check if signature is R(*func)(Args...) 1853 | constexpr bool is_free = is_free_function_pointer::value; 1854 | // check if signature is R(T::*func)(Args...) 1855 | constexpr bool is_member = std::is_member_function_pointer::value; 1856 | 1857 | static_assert(is_free || is_member, "The non-type template argument is expected to be of a free function or a compatible member function pointer type."); 1858 | 1859 | auto&& bindings = FunctionBindings::value[ArgType::class_name]; 1860 | bindings.push_back({ 1861 | name, 1862 | Function::signature, 1863 | is_member, 1864 | callable(args_t{}), 1865 | Function::kotlin_type 1866 | }); 1867 | return *this; 1868 | } 1869 | }; 1870 | 1871 | /** 1872 | * Declares a class to serve as a data transfer type. 1873 | * Data classes marshal data between native and Java code with copy semantics. The lifecycle of the native 1874 | * and the Java object is not coupled. 1875 | */ 1876 | template 1877 | struct data_class { 1878 | data_class() = default; 1879 | data_class(const data_class&) = delete; 1880 | data_class(data_class&&) = delete; 1881 | 1882 | template 1883 | data_class& field(const char* name) { 1884 | static_assert(std::is_member_object_pointer_v, "The non-type template argument is expected to be of a member variable pointer type."); 1885 | using member_type = typename FieldType::type; 1886 | 1887 | auto&& bindings = FieldBindings::value[ArgType::type_sig]; 1888 | bindings.push_back({ 1889 | name, 1890 | ArgType::type_sig, 1891 | [](JNIEnv* env, jobject obj, Field& fld, const void* native_object_ptr) { 1892 | const T* native_object = reinterpret_cast(native_object_ptr); 1893 | ArgType::java_field_value(env, obj, fld, native_object->*member); 1894 | }, 1895 | [](JNIEnv* env, jobject obj, Field& fld, void* native_object_ptr) { 1896 | T* native_object = reinterpret_cast(native_object_ptr); 1897 | native_object->*member = ArgType::native_field_value(env, obj, fld); 1898 | } 1899 | }); 1900 | return *this; 1901 | } 1902 | }; 1903 | 1904 | /** 1905 | * Prints all registered Java bindings. 1906 | */ 1907 | inline void print_registered_bindings() { 1908 | JavaOutput output(this_thread.getEnv()); 1909 | std::ostream& os = output.stream(); 1910 | 1911 | os 1912 | << "/** Represents a class that is instantiated in native code. */\n" 1913 | << "abstract class NativeObject : AutoCloseable {\n" 1914 | << " /** Holds an opaque reference to an object that exists in the native code execution context. */\n" 1915 | << " @Suppress(\"unused\") private val nativePointer: Long = 0\n" 1916 | << "}\n\n" 1917 | ; 1918 | 1919 | for (auto&& [class_name, bindings] : FunctionBindings::value) { 1920 | std::string simple_class_name; 1921 | std::size_t found = class_name.rfind('/'); 1922 | if (found != std::string::npos) { 1923 | simple_class_name = class_name.substr(found + 1); 1924 | } else { 1925 | simple_class_name = class_name; 1926 | } 1927 | 1928 | // class definition 1929 | os 1930 | << "class " << simple_class_name << " private constructor() : NativeObject() {\n" 1931 | ; 1932 | 1933 | // instance methods 1934 | for (auto&& binding : bindings) { 1935 | if (binding.is_member) { 1936 | os << " external fun " << binding.name << binding.friendly_signature << "\n"; 1937 | } 1938 | } 1939 | 1940 | // companion object methods 1941 | os << " companion object {\n"; 1942 | for (auto&& binding : bindings) { 1943 | if (!binding.is_member) { 1944 | os << " @JvmStatic external fun " << binding.name << binding.friendly_signature << "\n"; 1945 | } 1946 | } 1947 | 1948 | // end of class definition 1949 | os 1950 | << " }\n" 1951 | << "}\n"; 1952 | } 1953 | } 1954 | 1955 | inline void throw_exception(JNIEnv* env, const std::string& reason) { 1956 | if (env->ExceptionCheck()) { 1957 | env->ExceptionClear(); // thrown by a previously failed Java call 1958 | } 1959 | LocalClassRef exClass(env, "java/lang/Exception"); 1960 | env->ThrowNew(exClass.ref(), reason.c_str()); 1961 | } 1962 | } 1963 | 1964 | /** 1965 | * Implements the Java [JNI_OnLoad] initialization routine. 1966 | * @param initializer A user-defined function where bindings are registered, e.g. with [native_class]. 1967 | */ 1968 | inline jint java_initialization_impl(JavaVM* vm, void (*initializer)()) { 1969 | using namespace java; 1970 | 1971 | JNIEnv* env; 1972 | jint rc = vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); 1973 | if (rc != JNI_OK) { 1974 | return rc; 1975 | } 1976 | 1977 | // register Java environment 1978 | java::Environment::load(vm); 1979 | java::this_thread.setEnv(env); 1980 | 1981 | try { 1982 | // invoke user-defined function 1983 | initializer(); 1984 | 1985 | // register function bindings 1986 | for (auto&& [class_name, bindings] : FunctionBindings::value) { 1987 | // find the native class; JNI_OnLoad is called from the correct class loader context for this to work 1988 | LocalClassRef cls(env, class_name.data(), std::nothrow); 1989 | if (cls.ref() == nullptr) { 1990 | java::throw_exception(env, 1991 | msg() << "Cannot find class '" << class_name << "' registered as a native class in C++ code" 1992 | ); 1993 | return JNI_ERR; 1994 | } 1995 | 1996 | // register native methods of the class 1997 | std::vector functions; 1998 | std::transform(bindings.begin(), bindings.end(), std::back_inserter(functions), [](auto&& binding) -> JNINativeMethod { 1999 | return { 2000 | const_cast(binding.name.data()), 2001 | const_cast(binding.signature.data()), 2002 | binding.function_entry_point 2003 | }; 2004 | }); 2005 | jint rc = env->RegisterNatives(cls.ref(), functions.data(), functions.size()); 2006 | if (rc != JNI_OK) { 2007 | return rc; 2008 | } 2009 | } 2010 | 2011 | if (env->ExceptionCheck()) { 2012 | return JNI_ERR; 2013 | } 2014 | 2015 | // check property bindings 2016 | for (auto&& [class_name, bindings] : FieldBindings::value) { 2017 | // find the native class; JNI_OnLoad is called from the correct class loader context for this to work 2018 | LocalClassRef cls(env, class_name.data(), std::nothrow); 2019 | if (cls.ref() == nullptr) { 2020 | java::throw_exception(env, 2021 | msg() << "Cannot find class '" << class_name << "' registered as a data class in C++ code" 2022 | ); 2023 | return JNI_ERR; 2024 | } 2025 | 2026 | // try to look up registered fields 2027 | for (auto&& binding : bindings) { 2028 | jfieldID ref = env->GetFieldID(cls.ref(), binding.name.data(), binding.signature.data()); 2029 | if (ref != nullptr) { 2030 | continue; // everything OK, field exists 2031 | } 2032 | 2033 | java::throw_exception(env, 2034 | msg() << "Cannot find field '" << binding.name << "' with type signature '" << binding.signature << "' in registered class '" << class_name << "'" 2035 | ); 2036 | return JNI_ERR; 2037 | } 2038 | } 2039 | } catch (std::exception& ex) { 2040 | // ensure no native exception is propagated to Java 2041 | return JNI_ERR; 2042 | } 2043 | 2044 | return JNI_VERSION_1_6; 2045 | } 2046 | 2047 | /** 2048 | * Implements the Java [JNI_OnUnload] termination routine. 2049 | */ 2050 | inline void java_termination_impl(JavaVM* vm) { 2051 | java::Environment::unload(vm); 2052 | } 2053 | 2054 | /** 2055 | * Establishes a mapping between a composite native type and a Java data class. 2056 | * This object serves as a means to marshal data between Java and native, and is passed by value. 2057 | */ 2058 | #define DECLARE_DATA_CLASS(native_type, java_class_qualifier) \ 2059 | namespace java { \ 2060 | template <> \ 2061 | struct ArgType : DataClassArgType { \ 2062 | constexpr static std::string_view qualified_name = java_class_qualifier; \ 2063 | constexpr static std::string_view class_name = replace_v; \ 2064 | }; \ 2065 | } 2066 | 2067 | /** 2068 | * Establishes a mapping between a composite native type and a Java class. 2069 | * This object lives primarily in the native code space, and exposed to Java as an opaque handle. 2070 | */ 2071 | #define DECLARE_NATIVE_CLASS(native_type, java_class_qualifier) \ 2072 | namespace java { \ 2073 | template <> \ 2074 | struct ArgType : NativeClassArgType { \ 2075 | constexpr static std::string_view qualified_name = java_class_qualifier; \ 2076 | constexpr static std::string_view class_name = replace_v; \ 2077 | }; \ 2078 | } 2079 | 2080 | /** 2081 | * Registers the library with Java, and binds user-defined native functions to Java instance and class methods. 2082 | */ 2083 | #define JAVA_EXTENSION_MODULE() \ 2084 | static void java_bindings_initializer(); \ 2085 | JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return java_initialization_impl(vm, java_bindings_initializer); } \ 2086 | JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) { java_termination_impl(vm); } \ 2087 | void java_bindings_initializer() 2088 | 2089 | #define JAVA_OUTPUT ::java::JavaOutput(::java::this_thread.getEnv()).stream() 2090 | --------------------------------------------------------------------------------