feat(core): bind the Android Core with gomobile

Changelog: Share link import and mrs rule sets
This commit is contained in:
chen08209
2026-10-08 16:09:04 +08:00
parent 6c661f662e
commit 2fa2f7f816
104 changed files with 5537 additions and 3077 deletions
+242 -53
View File
@@ -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 }))
}
+2
View File
@@ -0,0 +1,2 @@
-keep class go.** { *; }
-keep class com.follow.clash.core.mobile.** { *; }
-28
View File
@@ -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)
-198
View File
@@ -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;
}
-133
View File
@@ -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);
}
-39
View File
@@ -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
}