Compare commits

..

No commits in common. "main" and "1.0.3" have entirely different histories.
main ... 1.0.3

13 changed files with 420 additions and 875 deletions

View file

@ -1,30 +0,0 @@
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 21 ]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
reqwest-jni
- run: cargo install cross
- name: set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: zulu
cache: "gradle"
- name: Run Build
run: ./gradlew shadowJar

View file

@ -12,18 +12,17 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: | workspaces: |
reqwest-jni reqwest-jni
- run: cargo install cross - run: cargo install cross
- name: set up JDK - name: set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: 21 java-version: 17
distribution: zulu distribution: temurin
check-latest: true check-latest: true
cache: "gradle" cache: "gradle"
- name: Save Private Key - name: Save Private Key

View file

@ -3,7 +3,7 @@ plugins {
id "maven-publish" id "maven-publish"
id "signing" id "signing"
id "fr.stardustenterprises.rust.importer" version "3.2.5" id "fr.stardustenterprises.rust.importer" version "3.2.5"
id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.johnrengelman.shadow' version '7.1.2'
} }
repositories { repositories {
@ -17,14 +17,14 @@ dependencies {
// javac -h // javac -h
tasks.register('generateJniHeaders', JavaCompile) { tasks.register('generateJniHeaders', JavaCompile) {
classpath = sourceSets.main.compileClasspath classpath = sourceSets.main.compileClasspath
destinationDir file("${layout.buildDirectory}/generated/jni") destinationDir file("${buildDir}/generated/jni")
source = sourceSets.main.java source = sourceSets.main.java
options.compilerArgs += [ options.compilerArgs += [
'-h', file("${layout.buildDirectory}/generated/jni"), '-h', file("${buildDir}/generated/jni"),
'-d', file("${layout.buildDirectory}/generated/jni-classes"), '-d', file("${buildDir}/generated/jni-classes"),
] ]
doLast { doLast {
delete file("${layout.buildDirectory}/generated/jni-classes") delete file("${buildDir}/generated/jni-classes")
} }
} }
@ -36,8 +36,6 @@ rustImport {
java { java {
withSourcesJar() withSourcesJar()
withJavadocJar() withJavadocJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
} }
signing { signing {
@ -45,7 +43,9 @@ signing {
} }
group = 'rocks.kavin' group = 'rocks.kavin'
version = '1.0.14' version = '1.0.3'
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
publishing { publishing {
repositories { repositories {

Binary file not shown.

View file

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

34
gradlew vendored
View file

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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 # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -85,9 +83,10 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit # 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -134,13 +133,10 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
if ! command -v java >/dev/null 2>&1 which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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 Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -148,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -156,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -201,15 +197,11 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# Collect all arguments for the java command: # * put everything else in single quotes, so that it's not re-expanded.
# * 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 -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View file

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail

View file

@ -13,5 +13,6 @@
"automerge": true, "automerge": true,
"platformAutomerge": true "platformAutomerge": true
} }
] ],
"enabledManagers": ["cargo", "github-actions"]
} }

874
reqwest-jni/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,9 @@ edition = "2021"
[dependencies] [dependencies]
jni = "0.21.1" jni = "0.21.1"
reqwest = {version = "0.12.4", features = ["rustls-tls", "stream", "brotli", "gzip", "socks"], default-features = false} reqwest = {version = "0.11.16", features = ["rustls-tls", "stream", "brotli", "gzip"], default-features = false}
tokio = {version = "1.37.0", features = ["rt-multi-thread", "time"], default-features = false} tokio = {version = "1.27.0", features = ["full"]}
once_cell = "1.17.1"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[profile.release]
lto = true

View file

@ -8,5 +8,4 @@ rust {
targets += target("aarch64-unknown-linux-gnu", "libreqwest.so") targets += target("aarch64-unknown-linux-gnu", "libreqwest.so")
targets += target("x86_64-unknown-linux-gnu", "libreqwest.so") targets += target("x86_64-unknown-linux-gnu", "libreqwest.so")
targets += target("x86_64-pc-windows-gnu", "libreqwest.dll")
} }

View file

@ -1,55 +1,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use jni::JNIEnv;
use jni::objects::{JByteArray, JClass, JMap, JObject, JString}; use jni::objects::{JByteArray, JClass, JMap, JObject, JString};
use jni::sys::jobject; use jni::sys::jobject;
use jni::JNIEnv; use once_cell::sync::Lazy;
use reqwest::{Client, Method, Url}; use reqwest::{Client, Method, Url};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
static RUNTIME: OnceLock<Runtime> = OnceLock::new(); static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
static CLIENT: OnceLock<Client> = OnceLock::new(); static CLIENT: Lazy<Client> = Lazy::new(||
Client::builder()
#[no_mangle] .user_agent("Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0")
pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_init(
mut env: JNIEnv,
_: JClass,
proxy: JString,
user: JString,
pass: JString,
) {
let builder = Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0");
let builder = match env.get_string(&proxy) {
Ok(proxy) => {
let proxy = proxy.to_str().unwrap();
let proxy = reqwest::Proxy::all(proxy).unwrap();
let proxy = match env.get_string(&user) {
Ok(user) => {
let user = user.to_str().unwrap();
let pass = env.get_string(&pass).unwrap();
let pass = pass.to_str().unwrap();
proxy.basic_auth(user, pass)
}
Err(_) => proxy,
};
builder.proxy(proxy)
}
Err(_) => builder,
};
let client = builder
// timeout for establishing connection
.connect_timeout(Duration::from_secs(10))
// timeout for entire request, till body is read
.timeout(Duration::from_secs(30))
.build() .build()
.unwrap(); .unwrap()
CLIENT.set(client).unwrap(); );
RUNTIME.set(Runtime::new().unwrap()).unwrap();
}
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch( pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch(
@ -60,6 +24,7 @@ pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch(
body: JByteArray, body: JByteArray,
headers: JObject, headers: JObject,
) -> jobject { ) -> jobject {
// set method, url, body, headers // set method, url, body, headers
let method = Method::from_bytes(env.get_string(&method).unwrap().to_bytes()).unwrap(); let method = Method::from_bytes(env.get_string(&method).unwrap().to_bytes()).unwrap();
@ -67,11 +32,7 @@ pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch(
let url = url.to_str(); let url = url.to_str();
if url.is_err() { if url.is_err() {
env.throw_new( env.throw_new("java/lang/IllegalArgumentException", "Invalid URL provided, couldn't get string as UTF-8").unwrap();
"java/lang/IllegalArgumentException",
"Invalid URL provided, couldn't get string as UTF-8",
)
.unwrap();
return JObject::null().into_raw(); return JObject::null().into_raw();
} }
@ -82,34 +43,16 @@ pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch(
let mut headers = HashMap::new(); let mut headers = HashMap::new();
while let Some((key, value)) = java_headers.next(&mut env).unwrap() { while let Some((key, value)) = java_headers.next(&mut env).unwrap() {
headers.insert( headers.insert(
env.get_string(&JString::from(key)) env.get_string(&JString::from(key)).unwrap().to_str().unwrap().to_string(),
.unwrap() env.get_string(&JString::from(value)).unwrap().to_str().unwrap().to_string(),
.to_str()
.unwrap()
.to_string(),
env.get_string(&JString::from(value))
.unwrap()
.to_str()
.unwrap()
.to_string(),
); );
} }
let client = CLIENT.get(); let request = CLIENT.request(method, url);
if client.is_none() { let request = headers.into_iter().fold(request, |request, (key, value)| {
env.throw_new("java/lang/IllegalStateException", "Client not initialized") request.header(key, value)
.unwrap(); });
return JObject::null().into_raw();
}
let client = client.unwrap();
let request = client.request(method, url);
let request = headers
.into_iter()
.fold(request, |request, (key, value)| request.header(key, value));
let request = if body.is_empty() { let request = if body.is_empty() {
request request
@ -117,113 +60,48 @@ pub extern "system" fn Java_rocks_kavin_reqwest4j_ReqwestUtils_fetch(
request.body(body) request.body(body)
}; };
// `JNIEnv` cannot be sent between threads safely
let jvm = env.get_java_vm().unwrap();
let jvm = Arc::new(jvm);
// create CompletableFuture
let _future = env
.new_object("java/util/concurrent/CompletableFuture", "()V", &[])
.unwrap();
let future = env.new_global_ref(&_future).unwrap();
let future = Arc::new(future);
let runtime = RUNTIME.get().unwrap();
// send request in a async task
{
let jvm = Arc::clone(&jvm);
let future = Arc::clone(&future);
runtime.spawn(async move {
// send request // send request
let response = request.send().await; let response = RUNTIME.block_on(async {
request.send().await
});
if let Err(error) = response {
let error = error.to_string();
env.throw_new("java/lang/RuntimeException", error).unwrap();
return JObject::null().into_raw();
}
let response = response.unwrap();
match response {
Ok(response) => {
// get response // get response
let status = response.status().as_u16() as i32; let status = response.status().as_u16() as i32;
let final_url = response.url().to_string();
let response_headers = response.headers().clone();
let body = response.bytes().await.unwrap_or_default().to_vec();
// send response in a blocking task
runtime.spawn_blocking(move || {
let mut env = jvm.attach_current_thread().unwrap();
let final_url = env.new_string(final_url).unwrap();
let body = env.byte_array_from_slice(&body).unwrap();
let headers = env.new_object("java/util/HashMap", "()V", &[]).unwrap(); let headers = env.new_object("java/util/HashMap", "()V", &[]).unwrap();
let headers: JMap = JMap::from_env(&mut env, &headers).unwrap(); let headers: JMap = JMap::from_env(&mut env, &headers).unwrap();
response_headers.iter().for_each(|(key, value)| { response.headers().iter().for_each(|(key, value)| {
let key = env.new_string(key.as_str()).unwrap(); let key = env.new_string(key.as_str()).unwrap();
let value = env.new_string(value.to_str().unwrap()).unwrap(); let value = env.new_string(value.to_str().unwrap()).unwrap();
headers headers.put(&mut env, &JObject::from(key), &JObject::from(value)).unwrap();
.put(&mut env, &JObject::from(key), &JObject::from(value))
.unwrap();
}); });
// return response to CompletableFuture let final_url = response.url().to_string();
let response = env let final_url = env.new_string(final_url).unwrap();
.new_object(
"rocks/kavin/reqwest4j/Response", let body = RUNTIME.block_on(async {
"(ILjava/util/Map;[BLjava/lang/String;)V", response.bytes().await.unwrap_or_default().to_vec()
&[ });
let body = env.byte_array_from_slice(&body).unwrap();
// return response
let response = env.new_object("rocks/kavin/reqwest4j/Response", "(ILjava/util/Map;[BLjava/lang/String;)V", &[
status.into(), status.into(),
(&headers).into(), (&headers).into(),
(&body).into(), (&body).into(),
(&final_url).into(), (&final_url).into(),
], ]).unwrap();
)
.unwrap();
let future = future.as_obj(); response.into_raw()
env.call_method(
future,
"complete",
"(Ljava/lang/Object;)Z",
&[(&response).into()],
)
.unwrap();
});
}
Err(error) => {
// send error in a blocking task
runtime.spawn_blocking(move || {
let mut env = jvm.attach_current_thread().unwrap();
let error = error.to_string();
let error = env.new_string(error).unwrap();
// create Exception
let exception = env
.new_object(
"java/lang/Exception",
"(Ljava/lang/String;)V",
&[(&error).into()],
)
.unwrap();
let future = future.as_obj();
// pass error to CompletableFuture
env.call_method(
future,
"completeExceptionally",
"(Ljava/lang/Throwable;)Z",
&[(&exception).into()],
)
.unwrap();
});
}
}
});
}
_future.into_raw()
} }

View file

@ -1,61 +1,42 @@
package rocks.kavin.reqwest4j; package rocks.kavin.reqwest4j;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class ReqwestUtils { public class ReqwestUtils {
static { static {
String arch = switch (System.getProperty("os.arch")) { String arch;
case "aarch64" -> "aarch64";
case "amd64" -> "x86_64";
default -> throw new RuntimeException("Unsupported architecture");
};
String os = System.getProperty("os.name").toLowerCase(); switch (System.getProperty("os.arch")) {
case "aarch64":
String extension; arch = "aarch64";
String native_folder; break;
case "amd64":
if (os.contains("win")) { arch = "x86_64";
extension = ".dll"; break;
native_folder = "windows"; default:
} else if (os.contains("linux")) { throw new RuntimeException("Unsupported architecture");
extension = ".so";
native_folder = "linux";
} else {
throw new RuntimeException("OS not supported");
} }
File nativeFile; String fileName =
System.getProperty("java.io.tmpdir") +
try { File.separatorChar +
nativeFile = File.createTempFile("libreqwest", extension); "libreqwest_" + System.currentTimeMillis() + ".so";
nativeFile.deleteOnExit();
} catch (IOException e) {
throw new RuntimeException(e);
}
final var cl = ReqwestUtils.class.getClassLoader(); final var cl = ReqwestUtils.class.getClassLoader();
try ( try (var stream = cl.getResourceAsStream("META-INF/natives/linux/" + arch + "/libreqwest.so")) {
var stream = cl.getResourceAsStream("META-INF/natives/" + native_folder + "/" + arch + "/libreqwest" + extension); stream.transferTo(new java.io.FileOutputStream(fileName));
var fileOutputStream = new FileOutputStream(nativeFile)
) {
stream.transferTo(fileOutputStream);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
System.load(nativeFile.getAbsolutePath()); System.load(fileName);
} }
public static native void init(String proxy, String user, String pass); public static native Response fetch(String url, String method, byte[] body,
public static native CompletableFuture<Response> fetch(String url, String method, byte[] body,
Map<String, String> headers); Map<String, String> headers);
} }