Add support Java bindings

This add support for Java bindings in the bindings/java directory.
This commit is contained in:
김선우 2025-01-01 21:53:50 +09:00 committed by Pekka Enberg
parent 1c2e074c93
commit 370e1ca5c2
22 changed files with 1253 additions and 0 deletions

121
Cargo.lock generated
View file

@ -205,6 +205,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cast"
version = "0.3.0"
@ -220,6 +226,12 @@ dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -359,6 +371,16 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@ -1020,6 +1042,39 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "java-limbo"
version = "0.0.11"
dependencies = [
"anyhow",
"jni",
"lazy_static",
"limbo_core",
"rand",
]
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "js-sys"
version = "0.3.76"
@ -2417,6 +2472,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -2444,6 +2508,21 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
@ -2475,6 +2554,12 @@ dependencies = [
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -2487,6 +2572,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -2499,6 +2590,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -2517,6 +2614,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -2529,6 +2632,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -2541,6 +2650,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -2553,6 +2668,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"

View file

@ -3,6 +3,7 @@
[workspace]
resolver = "2"
members = [
"bindings/java",
"bindings/python",
"bindings/wasm",
"cli",

39
bindings/java/.gitignore vendored Normal file
View file

@ -0,0 +1,39 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

19
bindings/java/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "java-limbo"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "_limbo_java"
crate-type = ["cdylib"]
path = "rs_src/lib.rs"
[dependencies]
anyhow = "1.0"
limbo_core = { path = "../../core" }
jni = "0.21.1"
rand = { version = "0.8.5", features = [] }
lazy_static = "1.5.0"

7
bindings/java/Makefile Normal file
View file

@ -0,0 +1,7 @@
java_run: lib
export LIMBO_SYSTEM_PATH=../../target/debug && ./gradlew run
.PHONY: lib
lib:
cargo build

View file

@ -0,0 +1,31 @@
plugins {
java
application
}
group = "org.github.tursodatabase"
version = "0.0.1-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
application {
mainClass.set("org.github.tursodatabase.Main")
val limboSystemLibraryPath = System.getenv("LIMBO_SYSTEM_PATH")
if (limboSystemLibraryPath != null) {
applicationDefaultJvmArgs = listOf(
"-Djava.library.path=${System.getProperty("java.library.path")}:$limboSystemLibraryPath"
)
}
}
tasks.test {
useJUnitPlatform()
}

Binary file not shown.

View file

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

252
bindings/java/gradlew vendored Executable file
View file

@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
bindings/java/gradlew.bat vendored Normal file
View file

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1,84 @@
use crate::cursor::Cursor;
use jni::objects::JClass;
use jni::sys::jlong;
use jni::JNIEnv;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Connection {
pub(crate) conn: Arc<Mutex<Rc<limbo_core::Connection>>>,
pub(crate) io: Arc<limbo_core::PlatformIO>,
}
/// Returns a pointer to a `Cursor` object.
///
/// The Java application will pass this pointer to native functions,
/// which will use it to reference the `Cursor` object.
///
/// # Arguments
///
/// * `_env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `connection_ptr` - A pointer to the `Connection` object.
///
/// # Returns
///
/// A `jlong` representing the pointer to the newly created `Cursor` object.
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_cursor<'local>(
_env: JNIEnv<'local>,
_class: JClass<'local>,
connection_ptr: jlong,
) -> jlong {
let connection = to_connection(connection_ptr);
let cursor = Cursor {
array_size: 1,
conn: connection.clone(),
description: None,
rowcount: -1,
smt: None,
};
Box::into_raw(Box::new(cursor)) as jlong
}
/// Closes the connection and releases the associated resources.
///
/// This function is called from the Java side to close the connection
/// and free the memory allocated for the `Connection` object.
///
/// # Arguments
///
/// * `_env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `connection_ptr` - A pointer to the `Connection` object to be closed.
#[no_mangle]
pub unsafe extern "system" fn Java_org_github_tursodatabase_limbo_Connection_close<'local>(
_env: JNIEnv<'local>,
_class: JClass<'local>,
connection_ptr: jlong,
) {
let _boxed_connection = Box::from_raw(connection_ptr as *mut Connection);
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_commit<'local>(
_env: &mut JNIEnv<'local>,
_class: JClass<'local>,
_connection_id: jlong,
) {
unimplemented!()
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Connection_rollback<'local>(
_env: &mut JNIEnv<'local>,
_class: JClass<'local>,
_connection_id: jlong,
) {
unimplemented!()
}
fn to_connection(connection_ptr: jlong) -> &'static mut Connection {
unsafe { &mut *(connection_ptr as *mut Connection) }
}

View file

@ -0,0 +1,240 @@
use crate::connection::Connection;
use crate::errors::ErrorCode;
use crate::utils::row_to_obj_array;
use crate::{eprint_return, eprint_return_null};
use jni::errors::JniError;
use jni::objects::{JClass, JObject, JString};
use jni::sys::jlong;
use jni::JNIEnv;
use limbo_core::IO;
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Cursor {
/// This read/write attribute specifies the number of rows to fetch at a time with `.fetchmany()`.
/// It defaults to `1`, meaning it fetches a single row at a time.
pub(crate) array_size: i64,
pub(crate) conn: Connection,
/// The `.description` attribute is a read-only sequence of 7-item, each describing a column in the result set:
///
/// - `name`: The column's name (always present).
/// - `type_code`: The data type code (always present).
/// - `display_size`: Column's display size (optional).
/// - `internal_size`: Column's internal size (optional).
/// - `precision`: Numeric precision (optional).
/// - `scale`: Numeric scale (optional).
/// - `null_ok`: Indicates if null values are allowed (optional).
///
/// The `name` and `type_code` fields are mandatory; others default to `None` if not applicable.
///
/// This attribute is `None` for operations that do not return rows or if no `.execute*()` method has been invoked.
pub(crate) description: Option<Description>,
/// Read-only attribute that provides the number of modified rows for `INSERT`, `UPDATE`, `DELETE`,
/// and `REPLACE` statements; it is `-1` for other statements, including CTE queries.
/// It is only updated by the `execute()` and `executemany()` methods after the statement has run to completion.
/// This means any resulting rows must be fetched for `rowcount` to be updated.
pub(crate) rowcount: i64,
pub(crate) smt: Option<Arc<Mutex<limbo_core::Statement>>>,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) struct Description {
_name: String,
_type_code: String,
_display_size: Option<String>,
_internal_size: Option<String>,
_precision: Option<String>,
_scale: Option<String>,
_null_ok: Option<String>,
}
impl Debug for Cursor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cursor")
.field("array_size", &self.array_size)
.field("description", &self.description)
.field("rowcount", &self.rowcount)
.finish()
}
}
/// TODO: we should find a way to handle Error thrown by rust and how to handle those errors in java
#[no_mangle]
#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_execute<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
sql: JString<'local>,
) -> Result<(), JniError> {
let sql: String = env
.get_string(&sql)
.expect("Could not extract query")
.into();
let stmt_is_dml = stmt_is_dml(&sql);
if stmt_is_dml {
return eprint_return!(
"DML statements (INSERT/UPDATE/DELETE) are not fully supported in this version",
JniError::Other(ErrorCode::STATEMENT_IS_DML)
);
}
let cursor = to_cursor(cursor_ptr);
let conn_lock = match cursor.conn.conn.lock() {
Ok(lock) => lock,
Err(_) => return eprint_return!("Failed to acquire connection lock", JniError::Other(-1)),
};
match conn_lock.prepare(&sql) {
Ok(statement) => {
cursor.smt = Some(Arc::new(Mutex::new(statement)));
Ok(())
}
Err(e) => {
eprint_return!(
&format!("Failed to prepare statement: {:?}", e),
JniError::Other(-1)
)
}
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchOne<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
) -> JObject<'local> {
let cursor = to_cursor(cursor_ptr);
if let Some(smt) = &cursor.smt {
loop {
let mut smt_lock = match smt.lock() {
Ok(lock) => lock,
Err(_) => {
return eprint_return_null!(
"Failed to acquire statement lock",
JniError::Other(-1)
)
}
};
match smt_lock.step() {
Ok(limbo_core::StepResult::Row(row)) => {
return match row_to_obj_array(&mut env, &row) {
Ok(r) => r,
Err(e) => eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)),
}
}
Ok(limbo_core::StepResult::IO) => {
if let Err(e) = cursor.conn.io.run_once() {
return eprint_return_null!(
&format!("IO Error: {:?}", e),
JniError::Other(-1)
);
}
}
Ok(limbo_core::StepResult::Interrupt) => return JObject::null(),
Ok(limbo_core::StepResult::Done) => return JObject::null(),
Ok(limbo_core::StepResult::Busy) => {
return eprint_return_null!("Busy error", JniError::Other(-1));
}
Err(e) => {
return eprint_return_null!(
format!("Step error: {:?}", e),
JniError::Other(-1)
);
}
};
}
} else {
eprint_return_null!("No statement prepared for execution", JniError::Other(-1))
}
}
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Cursor_fetchAll<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
cursor_ptr: jlong,
) -> JObject<'local> {
let cursor = to_cursor(cursor_ptr);
if let Some(smt) = &cursor.smt {
let mut rows = Vec::new();
loop {
let mut smt_lock = match smt.lock() {
Ok(lock) => lock,
Err(_) => {
return eprint_return_null!(
"Failed to acquire statement lock",
JniError::Other(-1)
)
}
};
match smt_lock.step() {
Ok(limbo_core::StepResult::Row(row)) => match row_to_obj_array(&mut env, &row) {
Ok(r) => rows.push(r),
Err(e) => return eprint_return_null!(&format!("{:?}", e), JniError::Other(-1)),
},
Ok(limbo_core::StepResult::IO) => {
if let Err(e) = cursor.conn.io.run_once() {
return eprint_return_null!(
&format!("IO Error: {:?}", e),
JniError::Other(-1)
);
}
}
Ok(limbo_core::StepResult::Interrupt) => {
return JObject::null();
}
Ok(limbo_core::StepResult::Done) => {
break;
}
Ok(limbo_core::StepResult::Busy) => {
return eprint_return_null!("Busy error", JniError::Other(-1));
}
Err(e) => {
return eprint_return_null!(
format!("Step error: {:?}", e),
JniError::Other(-1)
);
}
};
}
let array_class = env
.find_class("[Ljava/lang/Object;")
.expect("Failed to find Object array class");
let result_array = env
.new_object_array(rows.len() as i32, array_class, JObject::null())
.expect("Failed to create new object array");
for (i, row) in rows.into_iter().enumerate() {
env.set_object_array_element(&result_array, i as i32, row)
.expect("Failed to set object array element");
}
result_array.into()
} else {
eprint_return_null!("No statement prepared for execution", JniError::Other(-1))
}
}
fn to_cursor(cursor_ptr: jlong) -> &'static mut Cursor {
unsafe { &mut *(cursor_ptr as *mut Cursor) }
}
fn stmt_is_dml(sql: &str) -> bool {
let sql = sql.trim();
let sql = sql.to_uppercase();
sql.starts_with("INSERT") || sql.starts_with("UPDATE") || sql.starts_with("DELETE")
}

View file

@ -0,0 +1,35 @@
use jni::errors::{Error, JniError};
#[derive(Debug, Clone)]
pub struct CustomError {
pub message: String,
}
/// This struct defines error codes that correspond to the constants defined in the
/// Java package `org.github.tursodatabase.exceptions.ErrorCode`.
///
/// These error codes are used to handle and represent specific error conditions
/// that may occur within the Rust code and need to be communicated to the Java side.
#[derive(Clone)]
pub struct ErrorCode;
impl ErrorCode {
pub const CONNECTION_FAILURE: i32 = -1;
pub const STATEMENT_IS_DML: i32 = -1;
}
impl From<jni::errors::Error> for CustomError {
fn from(value: Error) -> Self {
CustomError {
message: value.to_string(),
}
}
}
impl From<CustomError> for JniError {
fn from(value: CustomError) -> Self {
eprintln!("Error occurred: {:?}", value.message);
JniError::Other(-1)
}
}

View file

@ -0,0 +1,66 @@
mod connection;
mod cursor;
mod errors;
mod macros;
mod utils;
use crate::connection::Connection;
use crate::errors::ErrorCode;
use jni::errors::JniError;
use jni::objects::{JClass, JString};
use jni::sys::jlong;
use jni::JNIEnv;
use std::sync::{Arc, Mutex};
/// Establishes a connection to the database specified by the given path.
///
/// This function is called from the Java side to create a connection to the database.
/// It returns a pointer to the `Connection` object, which can be used in subsequent
/// native function calls.
///
/// # Arguments
///
/// * `env` - The JNI environment pointer.
/// * `_class` - The Java class calling this function.
/// * `path` - A `JString` representing the path to the database file.
///
/// # Returns
///
/// A `jlong` representing the pointer to the newly created `Connection` object,
/// or [ErrorCode::CONNECTION_FAILURE] if the connection could not be established.
#[no_mangle]
pub extern "system" fn Java_org_github_tursodatabase_limbo_Limbo_connect<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
path: JString<'local>,
) -> jlong {
connect_internal(&mut env, path).unwrap_or_else(|_| ErrorCode::CONNECTION_FAILURE as jlong)
}
#[allow(improper_ctypes_definitions, clippy::arc_with_non_send_sync)] // TODO: remove
fn connect_internal<'local>(
env: &mut JNIEnv<'local>,
path: JString<'local>,
) -> Result<jlong, JniError> {
let io = Arc::new(limbo_core::PlatformIO::new().map_err(|e| {
println!("IO initialization failed: {:?}", e);
JniError::Unknown
})?);
let path: String = env
.get_string(&path)
.expect("Failed to convert JString to Rust String")
.into();
let db = limbo_core::Database::open_file(io.clone(), &path).map_err(|e| {
println!("Failed to open database: {:?}", e);
JniError::Unknown
})?;
let conn = db.connect().clone();
let connection = Connection {
conn: Arc::new(Mutex::new(conn)),
io,
};
Ok(Box::into_raw(Box::new(connection)) as jlong)
}

View file

@ -0,0 +1,16 @@
// bindings/java/src/macros.rs
#[macro_export]
macro_rules! eprint_return {
($log:expr, $error:expr) => {{
eprintln!("{}", $log);
Err($error)
}};
}
#[macro_export]
macro_rules! eprint_return_null {
($log:expr, $error:expr) => {{
eprintln!("{}", $log);
JObject::null()
}};
}

View file

@ -0,0 +1,30 @@
use crate::errors::CustomError;
use jni::objects::{JObject, JValue};
use jni::JNIEnv;
pub(crate) fn row_to_obj_array<'local>(
env: &mut JNIEnv<'local>,
row: &limbo_core::Row,
) -> Result<JObject<'local>, CustomError> {
let obj_array =
env.new_object_array(row.values.len() as i32, "java/lang/Object", JObject::null())?;
for (i, value) in row.values.iter().enumerate() {
let obj = match value {
limbo_core::Value::Null => JObject::null(),
limbo_core::Value::Integer(i) => {
env.new_object("java/lang/Long", "(J)V", &[JValue::Long(*i)])?
}
limbo_core::Value::Float(f) => {
env.new_object("java/lang/Double", "(D)V", &[JValue::Double(*f)])?
}
limbo_core::Value::Text(s) => env.new_string(s)?.into(),
limbo_core::Value::Blob(b) => env.byte_array_from_slice(b)?.into(),
};
if let Err(e) = env.set_object_array_element(&obj_array, i as i32, obj) {
eprintln!("Error on parsing row: {:?}", e);
}
}
Ok(obj_array.into())
}

View file

@ -0,0 +1 @@
rootProject.name = "limbo"

View file

@ -0,0 +1,16 @@
package org.github.tursodatabase;
import org.github.tursodatabase.limbo.Connection;
import org.github.tursodatabase.limbo.Cursor;
import org.github.tursodatabase.limbo.Limbo;
public class Main {
public static void main(String[] args) throws Exception {
Limbo limbo = Limbo.create();
Connection connection = limbo.getConnection("database.db");
Cursor cursor = connection.cursor();
cursor.execute("SELECT * FROM example_table;");
System.out.println("result: " + cursor.fetchOne());
}
}

View file

@ -0,0 +1,12 @@
package org.github.tursodatabase.exceptions;
/**
* This class defines error codes that correspond to specific error conditions
* that may occur while communicating with the JNI.
* <p />
* Refer to ErrorCode in rust package.
*/
public class ErrorCode {
public static int CONNECTION_FAILURE = -1;
}

View file

@ -0,0 +1,66 @@
package org.github.tursodatabase.limbo;
import java.lang.Exception;
/**
* Represents a connection to the database.
*/
public class Connection {
// Pointer to the connection object
private final long connectionPtr;
public Connection(long connectionPtr) {
this.connectionPtr = connectionPtr;
}
/**
* Creates a new cursor object using this connection.
*
* @return A new Cursor object.
* @throws Exception If the cursor cannot be created.
*/
public Cursor cursor() throws Exception {
long cursorId = cursor(connectionPtr);
return new Cursor(cursorId);
}
private native long cursor(long connectionPtr);
/**
* Closes the connection to the database.
*
* @throws Exception If there is an error closing the connection.
*/
public void close() throws Exception {
close(connectionPtr);
}
private native void close(long connectionPtr);
/**
* Commits the current transaction.
*
* @throws Exception If there is an error during commit.
*/
public void commit() throws Exception {
try {
commit(connectionPtr);
} catch (Exception e) {
System.out.println("caught exception: " + e);
}
}
private native void commit(long connectionPtr) throws Exception;
/**
* Rolls back the current transaction.
*
* @throws Exception If there is an error during rollback.
*/
public void rollback() throws Exception {
rollback(connectionPtr);
}
private native void rollback(long connectionPtr) throws Exception;
}

View file

@ -0,0 +1,85 @@
package org.github.tursodatabase.limbo;
/**
* Represents a database cursor.
*/
public class Cursor {
private long cursorPtr;
public Cursor(long cursorPtr) {
this.cursorPtr = cursorPtr;
}
// TODO: support parameters
public Cursor execute(String sql) {
var result = execute(cursorPtr, sql);
System.out.println("resut: " + result);
return this;
}
private static native int execute(long cursorPtr, String sql);
public Object fetchOne() throws Exception {
Object result = fetchOne(cursorPtr);
return processSingleResult(result);
}
private static native Object fetchOne(long cursorPtr);
public Object fetchAll() throws Exception {
Object result = fetchAll(cursorPtr);
return processArrayResult(result);
}
private static native Object fetchAll(long cursorPtr);
private Object processSingleResult(Object result) throws Exception {
if (result instanceof Object[]) {
System.out.println("The result is of type: Object[]");
for (Object element : (Object[]) result) {
printElementType(element);
}
return result;
} else {
printElementType(result);
return result;
}
}
private Object processArrayResult(Object result) throws Exception {
if (result instanceof Object[][]) {
System.out.println("The result is of type: Object[][]");
Object[][] array = (Object[][]) result;
for (Object[] row : array) {
for (Object element : row) {
printElementType(element);
}
}
return array;
} else {
throw new Exception("result should be of type Object[][]. Maybe internal logic has error.");
}
}
private void printElementType(Object element) {
if (element instanceof String) {
System.out.println("String: " + element);
} else if (element instanceof Integer) {
System.out.println("Integer: " + element);
} else if (element instanceof Double) {
System.out.println("Double: " + element);
} else if (element instanceof Boolean) {
System.out.println("Boolean: " + element);
} else if (element instanceof Long) {
System.out.println("Long: " + element);
} else if (element instanceof byte[]) {
System.out.print("byte[]: ");
for (byte b : (byte[]) element) {
System.out.print(b + " ");
}
System.out.println();
} else {
System.out.println("Unknown type: " + element);
}
}
}

View file

@ -0,0 +1,31 @@
package org.github.tursodatabase.limbo;
import org.github.tursodatabase.exceptions.ErrorCode;
import java.lang.Exception;
public class Limbo {
private static volatile boolean initialized;
private Limbo() {
if (!initialized) {
System.loadLibrary("_limbo_java");
initialized = true;
}
}
public static Limbo create() {
return new Limbo();
}
public Connection getConnection(String path) throws Exception {
long connectionId = connect(path);
if (connectionId == ErrorCode.CONNECTION_FAILURE) {
throw new Exception("Failed to initialize connection");
}
return new Connection(connectionId);
}
private static native long connect(String path);
}