feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
This commit is contained in:
+242
-53
@@ -1,18 +1,15 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
// Must equal Target.forPlatform("android") in setup_hooks, which writes libclash.so only for the platforms being built.
|
||||
val coreAbiByPlatform =
|
||||
linkedMapOf("android-arm" to "armeabi-v7a", "android-arm64" to "arm64-v8a", "android-x64" to "x86_64")
|
||||
val coreAbis =
|
||||
(rootProject.findProperty("target-platform") as String?)
|
||||
?.split(",")
|
||||
?.map { coreAbiByPlatform[it.trim()] ?: throw GradleException("No Core for Flutter target platform $it") }
|
||||
?: coreAbiByPlatform.values.toList()
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.core"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
@@ -20,17 +17,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
|
||||
ndk {
|
||||
abiFilters += coreAbis
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
// :app strips what it packages; Crashlytics symbolication needs the unstripped libraries to reach its merge.
|
||||
@@ -44,7 +31,6 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -53,42 +39,245 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.annotation.jvm)
|
||||
}
|
||||
val gomobileTargetByPlatform =
|
||||
linkedMapOf("android-arm" to "android/arm", "android-arm64" to "android/arm64", "android-x64" to "android/amd64")
|
||||
val gomobileTargets =
|
||||
(rootProject.findProperty("target-platform") as String?)
|
||||
?.split(",")
|
||||
?.map { gomobileTargetByPlatform[it.trim()] ?: throw GradleException("No Core for Flutter target platform $it") }
|
||||
?: gomobileTargetByPlatform.values.toList()
|
||||
|
||||
// Why prefix matching and mustRunAfter: .agents/architecture.md, "Android Native Task Ordering".
|
||||
val nativeTaskPattern =
|
||||
Regex("^(configureCMake|buildCMake|externalNativeBuild|merge.*(NativeLibs|JniLibFolders)|copy.*JniLibs)")
|
||||
val flutterCompileTasks =
|
||||
rootProject.project(":app").tasks.matching { it.name.startsWith("compileFlutterBuild") }
|
||||
val isWindows = System.getProperty("os.name").startsWith("Windows")
|
||||
|
||||
tasks.matching { nativeTaskPattern.containsMatchIn(it.name) }.configureEach {
|
||||
mustRunAfter(flutterCompileTasks)
|
||||
}
|
||||
// Gradle started from Android Studio sees the PATH of the GUI session, which leaves out Homebrew and the Go installer.
|
||||
val goExecutablePath =
|
||||
providers.provider {
|
||||
val name = if (isWindows) "go.exe" else "go"
|
||||
val home = System.getProperty("user.home")
|
||||
val searchPath =
|
||||
System.getenv("PATH").orEmpty().split(File.pathSeparator) +
|
||||
listOf("/opt/homebrew/bin", "/usr/local/bin", "/usr/local/go/bin", "$home/go/bin")
|
||||
searchPath
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { File(it, name) }
|
||||
.firstOrNull { it.canExecute() }
|
||||
?.path
|
||||
?: throw GradleException("The Android Core is built with gomobile, which needs Go on PATH")
|
||||
}
|
||||
|
||||
gradle.taskGraph.whenReady {
|
||||
val nativeTask =
|
||||
allTasks.firstOrNull {
|
||||
it.path.startsWith("${project.path}:") && nativeTaskPattern.containsMatchIn(it.name)
|
||||
val goModule = rootProject.layout.projectDirectory.dir("../core")
|
||||
|
||||
// gomobile names the library gojni, both as the file it builds and in go.Seq's System.loadLibrary, and has no option to change it.
|
||||
val coreLibraryName = "clash"
|
||||
|
||||
val buildConfig =
|
||||
providers.fileContents(rootProject.layout.projectDirectory.file("../build_config.yaml")).asText.map { text ->
|
||||
text.lines()
|
||||
.mapNotNull { Regex("""^(\w+):\s*(.*?)\s*$""").find(it)?.destructured }
|
||||
.associate { (key, value) -> key to value.trim('"', '\'') }
|
||||
}
|
||||
|
||||
abstract class GomobileBind : DefaultTask() {
|
||||
@get:Inject
|
||||
abstract val execOperations: ExecOperations
|
||||
|
||||
@get:InputFiles
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val sources: ConfigurableFileCollection
|
||||
|
||||
@get:Internal
|
||||
abstract val moduleDirectory: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val goExecutable: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val goVersion: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val targets: ListProperty<String>
|
||||
|
||||
@get:Input
|
||||
abstract val androidApi: Property<Int>
|
||||
|
||||
@get:Input
|
||||
abstract val tags: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val ldflags: Property<String>
|
||||
|
||||
@get:Internal
|
||||
abstract val sdkDirectory: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val ndkDirectory: DirectoryProperty
|
||||
|
||||
@get:Input
|
||||
abstract val ndkVersion: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val libraryName: Property<String>
|
||||
|
||||
@get:OutputFile
|
||||
abstract val aar: RegularFileProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val classesJar: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun bind() {
|
||||
val go = goExecutable.get()
|
||||
val tools = temporaryDir.resolve("bin")
|
||||
val executableSuffix = if (System.getProperty("os.name").startsWith("Windows")) ".exe" else ""
|
||||
tools.mkdirs()
|
||||
val path =
|
||||
listOf(tools, File(System.getProperty("java.home"), "bin"), File(go).parentFile)
|
||||
.joinToString(File.pathSeparator) { it.path } + File.pathSeparator + System.getenv("PATH").orEmpty()
|
||||
execOperations.exec {
|
||||
workingDir(moduleDirectory)
|
||||
environment("PATH", path)
|
||||
commandLine(go, "build", "-o", tools.path + File.separator, "golang.org/x/mobile/cmd/gomobile", "golang.org/x/mobile/cmd/gobind")
|
||||
}
|
||||
// gomobile links the library as the main package of a module it generates, whose go line is the toolchain's,
|
||||
// so the GODEBUG defaults that the go line of core/go.mod sets for the desktop Core would not reach it on their own.
|
||||
val godebug = ByteArrayOutputStream()
|
||||
execOperations.exec {
|
||||
workingDir(moduleDirectory)
|
||||
environment("PATH", path)
|
||||
commandLine(go, "list", "-f", "{{.DefaultGODEBUG}}", "./cmd/core")
|
||||
standardOutput = godebug
|
||||
}
|
||||
execOperations.exec {
|
||||
workingDir(moduleDirectory)
|
||||
environment("PATH", path)
|
||||
environment("ANDROID_HOME", sdkDirectory.get().asFile.path)
|
||||
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.path)
|
||||
commandLine(
|
||||
tools.resolve("gomobile$executableSuffix").path,
|
||||
"bind",
|
||||
"-target=${targets.get().joinToString(",")}",
|
||||
"-androidapi=${androidApi.get()}",
|
||||
"-javapkg=com.follow.clash.core",
|
||||
"-tags=${tags.get()}",
|
||||
"-ldflags=${ldflags.get()} -X=runtime.godebugDefault=${godebug.toString().trim()}",
|
||||
"-o",
|
||||
aar.get().asFile.path,
|
||||
"./mobile",
|
||||
)
|
||||
}
|
||||
ZipFile(aar.get().asFile).use { aarFile ->
|
||||
ZipInputStream(aarFile.getInputStream(aarFile.getEntry("classes.jar"))).use { jar ->
|
||||
ZipOutputStream(classesJar.get().asFile.outputStream()).use { out ->
|
||||
var seqPatched = false
|
||||
generateSequence { jar.nextEntry }.forEach { entry ->
|
||||
var bytes = jar.readBytes()
|
||||
if (entry.name == "go/Seq.class") {
|
||||
bytes = bytes.replaceUtf8Constant("gojni", libraryName.get())
|
||||
seqPatched = true
|
||||
}
|
||||
out.putNextEntry(ZipEntry(entry.name).apply { time = entry.time })
|
||||
out.write(bytes)
|
||||
}
|
||||
if (!seqPatched) throw GradleException("gomobile's classes.jar has no go/Seq.class to load lib${libraryName.get()}.so")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nativeTask == null || allTasks.any { it.path.startsWith(":app:compileFlutterBuild") }) {
|
||||
return@whenReady
|
||||
}
|
||||
val missing =
|
||||
coreAbis
|
||||
.flatMap {
|
||||
listOf(file("src/main/jniLibs/$it/libclash.so"), file("src/main/cpp/includes/$it/libclash.h"))
|
||||
}.filterNot { it.isFile }
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"${nativeTask.name} consumes Core artifacts that the setup build hook writes into src/main, " +
|
||||
"and these are absent: ${missing.joinToString { it.relativeTo(projectDir).path }}. " +
|
||||
"Run `dart setup.dart android` once, or build through :app so the hook runs first.",
|
||||
)
|
||||
|
||||
private fun ByteArray.replaceUtf8Constant(old: String, new: String): ByteArray {
|
||||
fun constant(value: String) = value.toByteArray().let { byteArrayOf(1, (it.size shr 8).toByte(), it.size.toByte()) + it }
|
||||
val target = constant(old)
|
||||
val matches = (0..size - target.size).filter { start -> target.indices.all { this[start + it] == target[it] } }
|
||||
if (matches.size != 1) throw GradleException("Expected one \"$old\" constant in go/Seq.class, found ${matches.size}")
|
||||
val start = matches.single()
|
||||
return copyOfRange(0, start) + constant(new) + copyOfRange(start + target.size, size)
|
||||
}
|
||||
logger.warn(
|
||||
":core: ${nativeTask.name} is running without an :app Flutter compile task, " +
|
||||
"so the Core artifacts under src/main are whatever the last hook run left behind",
|
||||
)
|
||||
}
|
||||
|
||||
abstract class UnpackJniLibs : DefaultTask() {
|
||||
@get:Inject
|
||||
abstract val archiveOperations: ArchiveOperations
|
||||
|
||||
@get:Inject
|
||||
abstract val fileSystemOperations: FileSystemOperations
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val aar: RegularFileProperty
|
||||
|
||||
@get:Input
|
||||
abstract val libraryName: Property<String>
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDirectory: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun unpack() {
|
||||
var renamed = 0
|
||||
fileSystemOperations.sync {
|
||||
from(archiveOperations.zipTree(aar)) {
|
||||
include("jni/**")
|
||||
eachFile {
|
||||
path = path.removePrefix("jni/")
|
||||
if (name == "libgojni.so") {
|
||||
name = "lib${libraryName.get()}.so"
|
||||
renamed++
|
||||
}
|
||||
}
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
into(outputDirectory)
|
||||
}
|
||||
if (renamed == 0) throw GradleException("gomobile's AAR has no libgojni.so to rename")
|
||||
}
|
||||
}
|
||||
|
||||
val bindCore =
|
||||
tasks.register<GomobileBind>("bindCore") {
|
||||
sources.from(
|
||||
fileTree(goModule) {
|
||||
exclude("**/*_test.go", "**/testdata/**", "cmd/**", "Clash.Meta/.github/**")
|
||||
},
|
||||
)
|
||||
moduleDirectory.set(goModule)
|
||||
goExecutable.set(goExecutablePath)
|
||||
goVersion.set(
|
||||
goExecutablePath
|
||||
.flatMap { go -> providers.exec { commandLine(go, "env", "GOVERSION") }.standardOutput.asText }
|
||||
.map { it.trim() },
|
||||
)
|
||||
targets.set(gomobileTargets)
|
||||
androidApi.set(libs.versions.minSdk.get().toInt())
|
||||
tags.set(buildConfig.map { it["tags"].orEmpty() })
|
||||
// Crashlytics symbolicates native crashes from the unstripped library :app merges, so the Core keeps its symbols.
|
||||
ldflags.set(
|
||||
buildConfig.map { config ->
|
||||
val flags = config["go_ldflags"].orEmpty().split(" ").filter { it.isNotEmpty() && it != "-s" && it != "-w" }
|
||||
val version = config["core_version"].orEmpty()
|
||||
(flags + listOfNotNull(version.takeIf { it.isNotEmpty() }?.let { "-X=github.com/metacubex/mihomo/constant.Version=$it" }))
|
||||
.joinToString(" ")
|
||||
},
|
||||
)
|
||||
sdkDirectory.set(androidComponents.sdkComponents.sdkDirectory)
|
||||
ndkDirectory.set(androidComponents.sdkComponents.ndkDirectory)
|
||||
ndkVersion.set(libs.versions.ndkVersion)
|
||||
libraryName.set(coreLibraryName)
|
||||
aar.set(layout.buildDirectory.file("gomobile/core.aar"))
|
||||
classesJar.set(layout.buildDirectory.file("gomobile/classes.jar"))
|
||||
}
|
||||
|
||||
// A library cannot bundle a local AAR into its own, so the gomobile AAR reaches AGP as a classes jar and generated jniLibs.
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
val unpack =
|
||||
tasks.register<UnpackJniLibs>("unpack${variant.name.replaceFirstChar { it.uppercase() }}CoreJniLibs") {
|
||||
aar.set(bindCore.flatMap { it.aar })
|
||||
libraryName.set(coreLibraryName)
|
||||
}
|
||||
variant.sources.jniLibs?.addGeneratedSourceDirectory(unpack, UnpackJniLibs::outputDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(files(bindCore.flatMap { it.classesJar }))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-keep class go.** { *; }
|
||||
-keep class com.follow.clash.core.mobile.** { *; }
|
||||
@@ -1,28 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
project("core")
|
||||
|
||||
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
|
||||
|
||||
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
|
||||
|
||||
|
||||
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||
add_compile_options(-O3 -flto -g -fno-exceptions -fno-rtti)
|
||||
add_link_options(-flto -Wl,--gc-sections)
|
||||
endif ()
|
||||
|
||||
set(LIB_CLASH_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/../cpp/includes/${ANDROID_ABI}")
|
||||
|
||||
# Never make linking libclash conditional on the file existing: AGP's configure
|
||||
# fingerprint tracks only CMakeLists.txt, so a configure that ran before the build
|
||||
# hook would stay cached and silently drop the Core. Failing at compile or link
|
||||
# time is what keeps that decision out of the configure step.
|
||||
include_directories(${LIB_CLASH_INCLUDE_DIR})
|
||||
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
clash)
|
||||
@@ -1,198 +0,0 @@
|
||||
#include <jni.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "jni_helper.h"
|
||||
#include "libclash.h"
|
||||
#include "bride.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
|
||||
jstring stack, jstring address, jstring dns) {
|
||||
const auto interface = new_global(cb);
|
||||
return startTUN(interface, fd, get_string(stack), get_string(address), get_string(dns))
|
||||
? JNI_TRUE
|
||||
: JNI_FALSE;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
stopTun();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
|
||||
forceGC();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
|
||||
updateDns(get_string(dns));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_invokeMethod(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
invokeMethod(interface, get_string(data));
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_setEventListener(JNIEnv *env, jobject thiz, jobject cb) {
|
||||
if (cb != nullptr) {
|
||||
const auto interface = new_global(cb);
|
||||
setEventListener(interface);
|
||||
} else {
|
||||
setEventListener(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string traffic = getTraffic(only_statistics_proxy);
|
||||
return new_string(traffic);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
|
||||
const jboolean only_statistics_proxy) {
|
||||
scoped_string traffic = getTotalTraffic(only_statistics_proxy);
|
||||
return new_string(traffic);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_quickSetup(JNIEnv *env, jobject thiz, jstring init_params_string,
|
||||
jstring setup_params_string, jobject cb) {
|
||||
const auto interface = new_global(cb);
|
||||
quickSetup(interface, get_string(init_params_string), get_string(setup_params_string));
|
||||
}
|
||||
|
||||
|
||||
static jmethodID m_tun_interface_protect;
|
||||
static jmethodID m_tun_interface_resolve_uid;
|
||||
static jmethodID m_tun_interface_resolve_package;
|
||||
static jmethodID m_invoke_interface_result;
|
||||
|
||||
|
||||
static void release_jni_object_impl(void *obj) {
|
||||
ATTACH_JNI();
|
||||
del_global(static_cast<jobject>(obj));
|
||||
}
|
||||
|
||||
static void free_string_impl(char *str) {
|
||||
free(str);
|
||||
}
|
||||
|
||||
static int call_tun_interface_protect_impl(void *tun_interface, const int fd) {
|
||||
// ART aborts the process on a call through a null object, so a callback
|
||||
// that was already released has to stop here rather than at the JNI call.
|
||||
if (tun_interface == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
ATTACH_JNI();
|
||||
const auto accepted = env->CallBooleanMethod(static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_protect,
|
||||
fd);
|
||||
if (jni_clear_exception(env)) {
|
||||
return 0;
|
||||
}
|
||||
return accepted == JNI_TRUE ? 1 : 0;
|
||||
}
|
||||
|
||||
static int call_tun_interface_resolve_uid_impl(void *tun_interface, const int protocol,
|
||||
const char *source,
|
||||
const char *target) {
|
||||
if (tun_interface == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
ATTACH_JNI();
|
||||
const auto source_string = new_string(source);
|
||||
const auto target_string = new_string(target);
|
||||
const auto uid = env->CallIntMethod(static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_uid,
|
||||
protocol,
|
||||
source_string,
|
||||
target_string);
|
||||
const auto failed = jni_clear_exception(env);
|
||||
if (source_string != nullptr) {
|
||||
env->DeleteLocalRef(source_string);
|
||||
}
|
||||
if (target_string != nullptr) {
|
||||
env->DeleteLocalRef(target_string);
|
||||
}
|
||||
return failed ? -1 : uid;
|
||||
}
|
||||
|
||||
static char *call_tun_interface_resolve_package_impl(void *tun_interface, const int uid) {
|
||||
if (tun_interface == nullptr) {
|
||||
return strdup("");
|
||||
}
|
||||
ATTACH_JNI();
|
||||
const auto package_name = reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||
static_cast<jobject>(tun_interface),
|
||||
m_tun_interface_resolve_package,
|
||||
uid));
|
||||
jni_clear_exception(env);
|
||||
const auto result = get_string(package_name);
|
||||
if (package_name != nullptr) {
|
||||
env->DeleteLocalRef(package_name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void call_invoke_interface_result_impl(void *invoke_interface, const char *data) {
|
||||
if (invoke_interface == nullptr) {
|
||||
return;
|
||||
}
|
||||
ATTACH_JNI();
|
||||
const auto value = new_bytes(data);
|
||||
env->CallVoidMethod(static_cast<jobject>(invoke_interface),
|
||||
m_invoke_interface_result,
|
||||
value);
|
||||
jni_clear_exception(env);
|
||||
if (value != nullptr) {
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
JNI_OnLoad(JavaVM *vm, void *) {
|
||||
JNIEnv *env = nullptr;
|
||||
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initialize_jni(vm, env);
|
||||
|
||||
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
|
||||
const auto c_invoke_interface = find_class("com/follow/clash/core/InvokeInterface");
|
||||
|
||||
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)Z");
|
||||
m_tun_interface_resolve_uid = find_method(c_tun_interface, "resolveUid",
|
||||
"(ILjava/lang/String;Ljava/lang/String;)I");
|
||||
m_tun_interface_resolve_package = find_method(c_tun_interface, "resolvePackage",
|
||||
"(I)Ljava/lang/String;");
|
||||
m_invoke_interface_result = find_method(c_invoke_interface, "onResult",
|
||||
"([B)V");
|
||||
|
||||
|
||||
protect_func = &call_tun_interface_protect_impl;
|
||||
resolve_uid_func = &call_tun_interface_resolve_uid_impl;
|
||||
resolve_package_func = &call_tun_interface_resolve_package_impl;
|
||||
result_func = &call_invoke_interface_result_impl;
|
||||
release_object_func = &release_jni_object_impl;
|
||||
free_string_func = &free_string_impl;
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#include "jni_helper.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <pthread.h>
|
||||
|
||||
static JavaVM *global_vm;
|
||||
|
||||
static jclass c_string;
|
||||
static jmethodID m_new_string;
|
||||
static jmethodID m_get_bytes;
|
||||
|
||||
static pthread_key_t detach_key;
|
||||
static bool detach_key_ready;
|
||||
|
||||
static void detach_current_thread(void *) {
|
||||
global_vm->DetachCurrentThread();
|
||||
}
|
||||
|
||||
void initialize_jni(JavaVM *vm, JNIEnv *env) {
|
||||
global_vm = vm;
|
||||
|
||||
detach_key_ready = pthread_key_create(&detach_key, detach_current_thread) == 0;
|
||||
|
||||
c_string = reinterpret_cast<jclass>(new_global(find_class("java/lang/String")));
|
||||
m_new_string = find_method(c_string, "<init>", "([B)V");
|
||||
m_get_bytes = find_method(c_string, "getBytes", "()[B");
|
||||
}
|
||||
|
||||
bool jni_clear_exception(JNIEnv *env) {
|
||||
if (env->ExceptionCheck() == JNI_FALSE) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
static char *empty_string() {
|
||||
return static_cast<char *>(calloc(1, 1));
|
||||
}
|
||||
|
||||
char *jni_get_string(JNIEnv *env, jstring str) {
|
||||
if (str == nullptr) {
|
||||
return empty_string();
|
||||
}
|
||||
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
|
||||
if (jni_clear_exception(env) || array == nullptr) {
|
||||
return empty_string();
|
||||
}
|
||||
const int length = env->GetArrayLength(array);
|
||||
const auto content = static_cast<char *>(malloc(length + 1));
|
||||
if (content == nullptr) {
|
||||
env->DeleteLocalRef(array);
|
||||
return empty_string();
|
||||
}
|
||||
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
|
||||
if (jni_clear_exception(env)) {
|
||||
// The copy did not happen, so `content` still holds whatever malloc
|
||||
// handed back. Returning it would pass that heap content on as the
|
||||
// string the caller asked for.
|
||||
free(content);
|
||||
env->DeleteLocalRef(array);
|
||||
return empty_string();
|
||||
}
|
||||
env->DeleteLocalRef(array);
|
||||
content[length] = 0;
|
||||
return content;
|
||||
}
|
||||
|
||||
jstring jni_new_string(JNIEnv *env, const char *str) {
|
||||
if (str == nullptr) {
|
||||
str = "";
|
||||
}
|
||||
const auto length = static_cast<int>(strlen(str));
|
||||
const auto array = env->NewByteArray(length);
|
||||
if (jni_clear_exception(env) || array == nullptr) {
|
||||
return env->NewStringUTF("");
|
||||
}
|
||||
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
|
||||
if (jni_clear_exception(env)) {
|
||||
// Calling NewObject with an exception still pending is undefined, and
|
||||
// the array it would read from was not filled in anyway.
|
||||
env->DeleteLocalRef(array);
|
||||
return env->NewStringUTF("");
|
||||
}
|
||||
const auto result = reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
|
||||
jni_clear_exception(env);
|
||||
env->DeleteLocalRef(array);
|
||||
if (result == nullptr) {
|
||||
return env->NewStringUTF("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
jbyteArray jni_new_bytes(JNIEnv *env, const char *str) {
|
||||
const auto length = str == nullptr ? 0 : static_cast<int>(strlen(str));
|
||||
const auto array = env->NewByteArray(length);
|
||||
if (jni_clear_exception(env) || array == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
|
||||
if (jni_clear_exception(env)) {
|
||||
env->DeleteLocalRef(array);
|
||||
return nullptr;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
void jni_attach_thread(scoped_jni *jni) {
|
||||
if (global_vm->GetEnv(reinterpret_cast<void **>(&jni->env), JNI_VERSION_1_6) == JNI_OK) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
if (global_vm->AttachCurrentThreadAsDaemon(&jni->env, nullptr) != JNI_OK) {
|
||||
abort();
|
||||
}
|
||||
if (detach_key_ready && pthread_setspecific(detach_key, jni->env) == 0) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
jni->require_release = 1;
|
||||
}
|
||||
|
||||
void jni_detach_thread(const scoped_jni *env) {
|
||||
if (env->require_release) {
|
||||
global_vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
void release_string(char **value) {
|
||||
free(*value);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
struct scoped_jni {
|
||||
JNIEnv *env;
|
||||
int require_release;
|
||||
};
|
||||
|
||||
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
|
||||
|
||||
extern jstring jni_new_string(JNIEnv *env, const char *str);
|
||||
|
||||
extern jbyteArray jni_new_bytes(JNIEnv *env, const char *str);
|
||||
|
||||
extern char *jni_get_string(JNIEnv *env, jstring str);
|
||||
|
||||
extern void jni_attach_thread(scoped_jni *jni);
|
||||
|
||||
extern void jni_detach_thread(const scoped_jni *env);
|
||||
|
||||
extern void release_string(char **value);
|
||||
|
||||
extern bool jni_clear_exception(JNIEnv *env);
|
||||
|
||||
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
|
||||
scoped_jni _jni{}; \
|
||||
jni_attach_thread(&_jni); \
|
||||
JNIEnv *env = _jni.env
|
||||
|
||||
#define scoped_string __attribute__((cleanup(release_string))) char*
|
||||
|
||||
#define find_class(name) env->FindClass(name)
|
||||
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
|
||||
#define new_global(obj) env->NewGlobalRef(obj)
|
||||
#define del_global(obj) env->DeleteGlobalRef(obj)
|
||||
#define get_string(jstr) jni_get_string(env, jstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
#define new_bytes(cstr) jni_new_bytes(env, cstr)
|
||||
@@ -1,23 +1,20 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import com.follow.clash.core.mobile.Mobile
|
||||
import com.follow.clash.core.mobile.ResultHandler
|
||||
import com.follow.clash.core.mobile.TunHandler
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URI
|
||||
|
||||
object Core {
|
||||
private external fun startTun(
|
||||
fd: Int,
|
||||
cb: TunInterface,
|
||||
stack: String,
|
||||
address: String,
|
||||
dns: String,
|
||||
): Boolean
|
||||
fun forceGC() = Mobile.forceGC()
|
||||
|
||||
external fun forceGC()
|
||||
|
||||
external fun updateDNS(
|
||||
fun updateDNS(
|
||||
dns: String,
|
||||
)
|
||||
) = Mobile.updateDns(dns)
|
||||
|
||||
fun resetNetwork() = Mobile.resetNetwork()
|
||||
|
||||
private fun parseInetSocketAddress(address: String): InetSocketAddress {
|
||||
val uri = URI("tcp://$address")
|
||||
@@ -34,10 +31,11 @@ object Core {
|
||||
stack: String,
|
||||
address: String,
|
||||
dns: String,
|
||||
options: String,
|
||||
): Boolean {
|
||||
return startTun(
|
||||
return Mobile.startTun(
|
||||
fd,
|
||||
object : TunInterface {
|
||||
object : TunHandler {
|
||||
override fun protect(fd: Int): Boolean = protect(fd)
|
||||
|
||||
override fun resolveUid(
|
||||
@@ -57,44 +55,23 @@ object Core {
|
||||
stack,
|
||||
address,
|
||||
dns,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
private external fun invokeMethod(
|
||||
data: String,
|
||||
cb: InvokeInterface,
|
||||
)
|
||||
|
||||
fun invokeMethod(
|
||||
data: String,
|
||||
cb: (result: ByteArray?) -> Unit,
|
||||
) {
|
||||
invokeMethod(
|
||||
data,
|
||||
object : InvokeInterface {
|
||||
override fun onResult(result: ByteArray?) {
|
||||
cb(result)
|
||||
}
|
||||
},
|
||||
)
|
||||
Mobile.invokeMethod(data, ResultHandler { result -> cb(result) })
|
||||
}
|
||||
|
||||
private external fun setEventListener(cb: InvokeInterface?)
|
||||
|
||||
fun updateEventListener(
|
||||
callback: ((result: String?) -> Unit)?,
|
||||
) {
|
||||
if (callback == null) {
|
||||
setEventListener(null)
|
||||
} else {
|
||||
setEventListener(
|
||||
object : InvokeInterface {
|
||||
override fun onResult(result: ByteArray?) {
|
||||
callback(result?.decodeToString())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Mobile.setEventListener(
|
||||
callback?.let { ResultHandler { result -> it(result?.decodeToString()) } },
|
||||
)
|
||||
}
|
||||
|
||||
fun quickSetup(
|
||||
@@ -102,30 +79,20 @@ object Core {
|
||||
setupParamsString: String,
|
||||
callback: (result: String?) -> Unit,
|
||||
) {
|
||||
quickSetup(
|
||||
Mobile.quickSetup(
|
||||
initParamsString,
|
||||
setupParamsString,
|
||||
object : InvokeInterface {
|
||||
override fun onResult(result: ByteArray?) {
|
||||
callback(result?.decodeToString())
|
||||
}
|
||||
},
|
||||
ResultHandler { result -> callback(result?.decodeToString()) },
|
||||
)
|
||||
}
|
||||
|
||||
private external fun quickSetup(
|
||||
initParamsString: String,
|
||||
setupParamsString: String,
|
||||
cb: InvokeInterface,
|
||||
)
|
||||
fun stopTun() = Mobile.stopTun()
|
||||
|
||||
external fun stopTun()
|
||||
fun startListener() = Mobile.startListener()
|
||||
|
||||
external fun getTraffic(onlyStatisticsProxy: Boolean): String
|
||||
fun stopListener() = Mobile.stopListener()
|
||||
|
||||
external fun getTotalTraffic(onlyStatisticsProxy: Boolean): String
|
||||
fun getTraffic(onlyStatisticsProxy: Boolean): String = Mobile.getTraffic(onlyStatisticsProxy)
|
||||
|
||||
init {
|
||||
System.loadLibrary("core")
|
||||
}
|
||||
fun getTotalTraffic(onlyStatisticsProxy: Boolean): String = Mobile.getTotalTraffic(onlyStatisticsProxy)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface InvokeInterface {
|
||||
fun onResult(result: ByteArray?)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface TunInterface {
|
||||
/** Returns false when the socket could not be kept out of the tunnel. */
|
||||
fun protect(fd: Int): Boolean
|
||||
|
||||
/** Returns the uid owning the connection, or -1 when the system will not name it. */
|
||||
fun resolveUid(protocol: Int, source: String, target: String): Int
|
||||
|
||||
fun resolvePackage(uid: Int): String
|
||||
}
|
||||
Reference in New Issue
Block a user