diff --git a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java index cc1242780..c2e6eac0b 100644 --- a/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java +++ b/module/incision/src/main/java/io/izzel/incision/bridge/IncisionBridge.java @@ -1,8 +1,13 @@ package io.izzel.incision.bridge; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -55,6 +60,16 @@ private IncisionBridge() {} private static final ConcurrentHashMap routeConflictWarnings = new ConcurrentHashMap(); + /** + * Side-car body 的字段解析缓存。 + * + * key 与 value 都必须是弱引用语义,避免系统级 Bridge 通过 Field 反向强持有插件 ClassLoader; + * 此处不能使用匿名 ClassValue 子类,因为 bootstrap 注入协议只复制 IncisionBridge.class, + * 任何 IncisionBridge$1.class 都会让 canonical 类初始化失败。 + */ + private static final Map, WeakReference>> accessFields = + Collections.synchronizedMap(new WeakHashMap, WeakReference>>()); + /** * 供 weaver 注入的 INVOKESTATIC 目标。 * @@ -133,6 +148,64 @@ public static boolean isBypassMiss(Object value) { return value == BYPASS_MISS; } + /** + * Side-car body 读取宿主私有字段的稳定入口。 + * 普通字段访问不应依赖某个插件 ClassLoader 独占的 JVMTI native image。 + */ + public static Object accessFieldGet(Object receiver, Class ownerClass, String fieldName, String fieldDesc) { + try { + return resolveAccessField(ownerClass, fieldName, fieldDesc).get(receiver); + } catch (Throwable t) { + throw new IllegalStateException("Incision field read failed: " + ownerClass.getName() + "." + fieldName, t); + } + } + + /** Side-car body 写入宿主私有字段;访问规则与 {@link #accessFieldGet} 相同。 */ + public static void accessFieldSet(Object receiver, Class ownerClass, String fieldName, String fieldDesc, Object value) { + try { + resolveAccessField(ownerClass, fieldName, fieldDesc).set(receiver, value); + } catch (Throwable t) { + throw new IllegalStateException("Incision field write failed: " + ownerClass.getName() + "." + fieldName, t); + } + } + + /** Side-car body 读取宿主私有静态字段。 */ + public static Object accessStaticFieldGet(Class ownerClass, String fieldName, String fieldDesc) { + return accessFieldGet(null, ownerClass, fieldName, fieldDesc); + } + + /** Side-car body 写入宿主私有静态字段。 */ + public static void accessStaticFieldSet(Class ownerClass, String fieldName, String fieldDesc, Object value) { + accessFieldSet(null, ownerClass, fieldName, fieldDesc, value); + } + + private static Field resolveAccessField(Class ownerClass, String fieldName, String fieldDesc) throws NoSuchFieldException { + String key = fieldName + ':' + fieldDesc; + ConcurrentHashMap fields; + synchronized (accessFields) { + WeakReference> reference = accessFields.get(ownerClass); + fields = reference == null ? null : reference.get(); + if (fields == null) { + fields = new ConcurrentHashMap(); + accessFields.put(ownerClass, new WeakReference>(fields)); + } + } + Field cached = fields.get(key); + if (cached != null) return cached; + Class cursor = ownerClass; + while (cursor != null) { + try { + Field field = cursor.getDeclaredField(fieldName); + field.setAccessible(true); + Field previous = fields.putIfAbsent(key, field); + return previous == null ? field : previous; + } catch (NoSuchFieldException ignored) { + cursor = cursor.getSuperclass(); + } + } + throw new NoSuchFieldException(ownerClass.getName() + '.' + fieldName + ':' + fieldDesc); + } + /** 宿主绑定入口 — GateBootstrapper 创建 host 后调用此方法完成注册 */ public static synchronized void bindSystemHost(Object host) { systemHost = host; diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt index 5b4fc9226..d991c064e 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/IncisionBootstrap.kt @@ -39,6 +39,10 @@ object IncisionBootstrap { /** 当前 incision API 版本,用于网关版本协商 */ const val API_VERSION = 2 + /** 系统属性名必须运行时拼接,避免插件 Shadow 把协议键误当作 taboolib 包名重定位。 */ + private val backendProperty = + String(charArrayOf('t', 'a', 'b', 'o', 'o', 'l', 'i', 'b')) + ".incision.backend" + init { prepareConst() } @@ -167,8 +171,10 @@ object IncisionBootstrap { Forensics.warn("IncisionBridge.class 资源未找到: $resourcePath") return } + // 显式选择 Instrumentation 时不得探测 native;System.load 的进程级副作用无法在插件 CL 间回滚。 + val forcedBackend = System.getProperty(backendProperty, "auto").lowercase() // 路径 1: JVMTI native — defineClass 直接注入 bootstrap CL - if (JvmtiBackend.available()) { + if (forcedBackend != "instrumentation" && JvmtiBackend.available()) { val cls = JvmtiBackend.defineClassInClassLoader(null, bridgeClassName, bytes) if (cls != null) { Forensics.info("IncisionBridge 已注入 bootstrap ClassLoader (JVMTI)") diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt index b6eb2437c..62d791f6c 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/diagnostic/Forensics.kt @@ -1,7 +1,9 @@ package taboolib.module.incision.diagnostic import taboolib.common.PrimitiveSettings -import taboolib.common.platform.function.warning +import taboolib.common.platform.function.debug as platformDebug +import taboolib.common.platform.function.info as platformInfo +import taboolib.common.platform.function.warning as platformWarning /** * 结构化诊断日志器。 @@ -18,15 +20,15 @@ object Forensics { get() = PrimitiveSettings.IS_DEBUG_MODE fun info(message: String) { - if (DEBUG) taboolib.common.platform.function.info("[Incision] $message") + if (DEBUG) emitSafely("[Incision] $message", false) { platformInfo(it) } } fun debug(message: String) { - if (DEBUG) taboolib.common.platform.function.debug("[Incision][DEBUG] $message") + if (DEBUG) emitSafely("[Incision][DEBUG] $message", false) { platformDebug(it) } } fun warn(message: String) { - if (DEBUG) warning("[Incision][WARN] $message") + if (DEBUG) emitSafely("[Incision][WARN] $message", true) { platformWarning(it) } } fun error(message: String, cause: Throwable? = null) { @@ -34,6 +36,18 @@ object Forensics { cause?.printStackTrace(System.err) } + /** + * CONST 阶段可能早于 BukkitPlugin 实例完成构造,平台日志实现此时会访问尚未就绪的插件实例。 + * 诊断路径绝不能反向中断 Incision 初始化,因此平台输出失败时只退回 JDK 标准流。 + */ + private fun emitSafely(message: String, stderr: Boolean, platformLog: (String) -> Unit) { + try { + platformLog(message) + } catch (_: Throwable) { + if (stderr) System.err.println(message) else System.out.println(message) + } + } + /** * 上报 Trauma — 输出结构化字段 + 触发栈。 */ diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt index be0fbd78f..081e4dc1d 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/dsl/Scalpel.kt @@ -192,7 +192,10 @@ object Scalpel { group.forEach { ownerEntries[it.id] = it } activeTokens.remove(resolvedOwner)?.remove() val targets = buildRuntimeTargets(resolvedOwner, ownerEntries.values.toList()) - val weaver = ScalpelWeaver(targetsByOwner = mapOf(resolvedOwner to targets)) + val weaver = ScalpelWeaver( + targetsByOwner = mapOf(resolvedOwner to targets), + useJvmtiBaseline = backend === JvmtiBackend, + ) val installation = backend.install(resolvedOwner) { bytes -> weaver.weave(bytes) } val token = installation.token if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD) || token == null) { @@ -200,7 +203,10 @@ object Scalpel { ownerEntries.putAll(previousEntries) if (previousEntries.isNotEmpty()) { val previousTargets = buildRuntimeTargets(resolvedOwner, previousEntries.values.toList()) - val previousWeaver = ScalpelWeaver(targetsByOwner = mapOf(resolvedOwner to previousTargets)) + val previousWeaver = ScalpelWeaver( + targetsByOwner = mapOf(resolvedOwner to previousTargets), + useJvmtiBaseline = backend === JvmtiBackend, + ) val restored = backend.install(resolvedOwner) { bytes -> previousWeaver.weave(bytes) } restored.token?.takeIf { restored.status == Backend.InstallStatus.INSTALLED || restored.status == Backend.InstallStatus.PENDING_LOAD @@ -252,7 +258,10 @@ object Scalpel { return backend.isClassLoaded(owner) == false || backend.retransform(owner.replace('/', '.')) } val targets = buildRuntimeTargets(owner, entries) - val weaver = ScalpelWeaver(targetsByOwner = mapOf(owner to targets)) + val weaver = ScalpelWeaver( + targetsByOwner = mapOf(owner to targets), + useJvmtiBaseline = backend === JvmtiBackend, + ) val installation = backend.install(owner) { bytes -> weaver.weave(bytes) } val token = installation.token ?: return false if (installation.status !in setOf(Backend.InstallStatus.INSTALLED, Backend.InstallStatus.PENDING_LOAD)) return false diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt index 0aa247174..7cb8728e7 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/AdviceCtx.kt @@ -4,8 +4,8 @@ package taboolib.module.incision.pred * 谓词编译上下文。由 advice 注册方提供,传给 [PredCompiler.compile]。 * * @property adviceId advice id,仅用于错误诊断(出现在 [taboolib.module.incision.diagnostic.Trauma.Predicate.RuntimeFailure] 中)。 - * @property classLoader 生成的谓词类的装载目标 ClassLoader。通常是声明 advice 的插件主 CL; - * 后续 script / external 场景可指向脚本沙箱 CL。 + * @property classLoader 生成谓词专用 ClassLoader 的 parent。通常是声明 advice 的插件主 CL; + * 后续 script / external 场景可指向脚本沙箱 CL,以限制可见类型边界。 * @property extraVars 除默认 `args/this/result/env/site/caller` 外允许出现的顶层变量名。 * 未列入白名单的变量会在编译期抛 [taboolib.module.incision.diagnostic.Trauma.Predicate.UndefinedVariable]。 */ diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt index 0eaef0817..6cc78a7e3 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/PredCompiler.kt @@ -5,8 +5,8 @@ import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes import taboolib.module.incision.diagnostic.Trauma -import taboolib.module.incision.loader.JvmtiBackend -import java.lang.reflect.Method +import java.lang.ref.WeakReference +import java.util.WeakHashMap import java.util.concurrent.atomic.AtomicInteger /** @@ -448,49 +448,44 @@ object PredCompiler { } } - // ---------- 类装载器:反射优先;JDK 9+ 模块边界失败则退到 JNI DefineClass ---------- + // ---------- 类装载器 ------------------------------------------------------- private object LoaderHelper { - private val defineMethod: Method? by lazy { - try { - val m = ClassLoader::class.java.getDeclaredMethod( - "defineClass", - String::class.java, - ByteArray::class.java, - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - ) - m.isAccessible = true - m - } catch (_: Throwable) { - null - } - } + + /** + * 每个 advice defining loader 对应一个弱引用生成类加载器。 + * + * 生成谓词只需要通过 parent 看见插件中的 Predicate/PredOps,并不需要强行定义进插件 + * ClassLoader。使用子加载器可以同时避开 JDK 9+ 对 ClassLoader#defineClass 的模块封装, + * 也避免为普通类定义误触 JVMTI native;弱键确保插件卸载后不会被全局缓存阻止回收。 + */ + private val generatedLoaders = WeakHashMap>() fun define(cl: ClassLoader, name: String, bytes: ByteArray): Class<*> { val binaryName = name.replace('/', '.') - try { - return Class.forName(binaryName, false, cl) - } catch (_: Throwable) { - } - val reflectError: Throwable? = defineMethod?.let { m -> - try { - return m.invoke(cl, binaryName, bytes, 0, bytes.size) as Class<*> - } catch (t: Throwable) { - t + val generatedLoader = synchronized(generatedLoaders) { + generatedLoaders[cl]?.get() ?: GeneratedPredicateClassLoader(cl).also { + // value 也必须是弱引用;GeneratedPredicateClassLoader.parent 会反向强持有 key, + // 若直接把 loader 作为 value,WeakHashMap 的弱键将永远无法回收。 + generatedLoaders[cl] = WeakReference(it) } } - // JDK 9+ 或反射被禁:退到 native JNI DefineClass(不受模块系统限制) - try { - val cls = JvmtiBackend.defineClassInClassLoader(cl, name, bytes) - if (cls != null) return cls - } catch (_: Throwable) { - // native 不可用时继续抛原反射异常 + return try { + generatedLoader.define(binaryName, bytes) + } catch (t: Throwable) { + throw Trauma.Predicate.RuntimeFailure("", null, t) } - throw Trauma.Predicate.RuntimeFailure( - "", null, - reflectError ?: IllegalStateException("defineClass unavailable (reflection + JVMTI both failed)") - ) + } + + /** + * defineClass 只能由 ClassLoader 子类合法调用;同步保证同一生成名称不会被并发重复定义。 + * 类名由全局递增序列生成,findLoadedClass 仍作为防御性检查保留。 + */ + private class GeneratedPredicateClassLoader(parent: ClassLoader) : ClassLoader(parent) { + + @Synchronized + fun define(binaryName: String, bytes: ByteArray): Class<*> = + findLoadedClass(binaryName) ?: defineClass(binaryName, bytes, 0, bytes.size) } } } diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt b/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt index 25c93f575..83a9a106c 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/pred/Predicate.kt @@ -3,7 +3,8 @@ package taboolib.module.incision.pred /** * 编译后的谓词。 * - * 由 [PredCompiler] 从 [PredAst] 生成 ASM 字节码并装载到目标 ClassLoader 后实例化。 + * 由 [PredCompiler] 从 [PredAst] 生成 ASM 字节码,并装载到以 advice ClassLoader 为 parent 的 + * 专用生成类加载器后实例化。专用 loader 避免依赖受模块封装限制的反射 defineClass 或 JVMTI。 * * 实现类要求: * - 无状态、线程安全(dispatcher 多线程并发 `test`) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt index 0d0435d0d..e2b126b14 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/BodiesClassGenerator.kt @@ -197,19 +197,11 @@ object BodiesClassGenerator { } /** - * JvmtiBackend 的 JVM 内部类名(全斜杠形式)。 - * - * 注意:绝不能写成 `const val "taboolib/module/incision/loader/JvmtiBackend"`。 - * 因为 const 字符串会被内联,TabooLib 的 Shadow relocation 会把其中的 `taboolib` - * token 按「包名(点号)」规则替换为重定位前缀,得到点斜混合的非法名,例如 - * `group.taboolib/module/incision/loader/JvmtiBackend`,导致生成的 Bodies 类在 - * defineClass 时抛 ClassFormatError: Illegal class name。 - * - * 改为运行期从已重定位的实际类对象推导:`Class.name` 在重定位后是正确的全限定名, - * 仅需把 `.` 换成 `/` 即可得到合法内部名,且对任意重定位前缀都成立。 + * canonical Bridge 固定存在于 bootstrap/system ClassLoader,服务端类与第三方插件类均可解析。 + * Side-car body 不能调用插件私有副本中的 JVMTI native:多插件各有独立 ClassLoader,而同一 + * native image 在 JVM 中只能由一个 loader 绑定,后续插件会得到 UnsatisfiedLinkError。 */ - private val JVMTI_BACKEND: String = - taboolib.module.incision.loader.JvmtiBackend::class.java.name.replace('.', '/') + private const val ACCESS_BRIDGE = "io/izzel/incision/bridge/IncisionBridge" /** * 克隆原方法指令流到 [out],做 slot 偏移、return 替换、private 字段访问替换。 @@ -219,7 +211,7 @@ object BodiesClassGenerator { * - xRETURN(基本类型):装箱 + ARETURN * - RETURN(void):ACONST_NULL + ARETURN * - ARETURN:保持 - * - GETFIELD/PUTFIELD 访问 private 字段:替换为 JNI 层 nFieldGet/nFieldSet + * - GETFIELD/PUTFIELD 访问 private 字段:替换为 canonical Bridge 的反射访问入口 */ private fun cloneInstructionsInto( out: InsnList, @@ -237,7 +229,7 @@ object BodiesClassGenerator { is IincInsnNode -> cloned.`var` += 2 } - // private 字段访问 → 通过 C 层 JNI 绕过访问控制 + // private 字段访问 → 通过系统级 Bridge 解析,避免 side-car 不具备宿主 nestmate 权限。 if (cloned is FieldInsnNode && cloned.owner == ownerInternal && cloned.name in privateFields) { when (cloned.opcode) { GETFIELD -> { @@ -247,7 +239,7 @@ object BodiesClassGenerator { out.add(LdcInsnNode(cloned.name)) out.add(LdcInsnNode(cloned.desc)) out.add(MethodInsnNode( - INVOKESTATIC, JVMTI_BACKEND, "nFieldGet", + INVOKESTATIC, ACCESS_BRIDGE, "accessFieldGet", "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", false )) @@ -271,7 +263,7 @@ object BodiesClassGenerator { out.add(LdcInsnNode(cloned.desc)) out.add(VarInsnNode(ALOAD, 0)) // 取回 boxedValue out.add(MethodInsnNode( - INVOKESTATIC, JVMTI_BACKEND, "nFieldSet", + INVOKESTATIC, ACCESS_BRIDGE, "accessFieldSet", "(Ljava/lang/Object;Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false )) @@ -283,7 +275,7 @@ object BodiesClassGenerator { out.add(LdcInsnNode(cloned.name)) out.add(LdcInsnNode(cloned.desc)) out.add(MethodInsnNode( - INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldGet", + INVOKESTATIC, ACCESS_BRIDGE, "accessStaticFieldGet", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", false )) @@ -301,7 +293,7 @@ object BodiesClassGenerator { out.add(LdcInsnNode(cloned.desc)) out.add(VarInsnNode(ALOAD, 0)) out.add(MethodInsnNode( - INVOKESTATIC, JVMTI_BACKEND, "nStaticFieldSet", + INVOKESTATIC, ACCESS_BRIDGE, "accessStaticFieldSet", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false )) diff --git a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt index 2286b755c..63c04d7c0 100644 --- a/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt +++ b/module/incision/src/main/kotlin/taboolib/module/incision/weaver/Scalpel.kt @@ -5,6 +5,9 @@ import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes import org.objectweb.asm.commons.AdviceAdapter +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode import taboolib.module.incision.api.MethodCoordinate import taboolib.module.incision.cache.IncisionCache import taboolib.module.incision.diagnostic.Forensics @@ -19,18 +22,21 @@ import java.io.File /** * 主 weaver — 接收一个目标类的字节码,针对若干 target 方法注入 dispatcher 调用。 * - * 注入的 JVM 调用签名(固定由 IncisionGate / TheatreDispatcher 持有): + * 注入的 JVM 调用签名(固定由 bootstrap/system ClassLoader 中的 IncisionBridge 持有): * ``` - * INVOKESTATIC taboolib/module/incision/runtime/TheatreDispatcher.dispatch - * (Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; + * INVOKESTATIC io/izzel/incision/bridge/IncisionBridge.dispatch + * (Ljava/lang/Class;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; * ``` - * 后续 GateBootstrapper 启用后会把调用重定向到 `taboolib/incision/gate/IncisionGate`。 * - * 当前支持 Lead / Trail / Splice / Excise;Graft / Bypass / Trim 标记为待实现。 + * 同一目标可能被多个插件声明。后注册 transformer 会收到前一个 transformer 的输出, + * 因此入口 advice 必须识别已有 Bridge 调用,保证 JVM 中只有一个物理入口,再由 Bridge + * 按注册顺序广播给所有声明该目标的 dispatcher。 */ class Scalpel( private val resolver: NameResolver = RemapRouter, private val targetsByOwner: Map>, + /** 仅 JVMTI backend 需要 native 原始字节缓存;Instrumentation 自己提供重转换基线。 */ + private val useJvmtiBaseline: Boolean = false, ) { data class AdviceTargetSpec( @@ -43,10 +49,12 @@ class Scalpel( return try { val probeReader = ClassReader(originalBytes) val probeOwner = probeReader.className - val sourceBytes = JvmtiBackend.getCachedOriginal(probeOwner) ?: run { - JvmtiBackend.cacheOriginal(probeOwner, originalBytes) - originalBytes - } + val sourceBytes = if (useJvmtiBaseline) { + JvmtiBackend.getCachedOriginal(probeOwner) ?: run { + JvmtiBackend.cacheOriginal(probeOwner, originalBytes) + originalBytes + } + } else originalBytes val sourceReader = ClassReader(sourceBytes) val owner = sourceReader.className val targets = targetsByOwner[owner] ?: return originalBytes @@ -60,7 +68,8 @@ class Scalpel( } val reader = ClassReader(bodySourceBytes) val writer = SafeClassWriter(reader, loader) - val visitor = WeavingClassVisitor(Opcodes.ASM9, writer, targets) + val existingEntryPhases = detectExistingEntryPhases(bodySourceBytes, targets) + val visitor = WeavingClassVisitor(Opcodes.ASM9, writer, targets, existingEntryPhases) // 只映射声明坐标,绝不能再次映射服务端已经转换过的整份字节码;后者会破坏 // Paper/CraftBukkit 自带的 relocated 依赖名称,并在 retransform 后污染运行中的类。 reader.accept(visitor, ClassReader.EXPAND_FRAMES) @@ -77,6 +86,9 @@ class Scalpel( } companion object { + private const val BRIDGE_OWNER = "io/izzel/incision/bridge/IncisionBridge" + private const val MAX_DISPATCH_PREFIX_INSNS = 64 + /** * 由 [InstrumentationBackend] 在 transform 回调中设置,携带目标类的 ClassLoader。 * [SafeClassWriter.getClassLoader] 读取此值,让 getCommonSuperClass 的 Class.forName @@ -306,6 +318,82 @@ class Scalpel( return false } + /** 方法键必须包含 descriptor,同名重载之间不能共享“已织入”判断。 */ + private data class MethodKey(val name: String, val descriptor: String) + + /** + * whole-method advice 的三个物理入口阶段。 + * SPLICE 同时承载 SPLICE/EXCISE 以及无 Site 的 GRAFT/BYPASS/TRIM,它们必须共用一个入口。 + */ + private enum class EntryPhase { LEAD, TRAIL, SPLICE } + + /** + * 扫描前序 transformer 已写入的入口。 + * + * Instrumentation 会把多个可重转换 transformer 串联执行,后一个插件拿到的 bytes 已包含 + * 前一个插件的输出。若再次发射相同 phase,单次方法调用会产生 N 个 Bridge 入口,而每个入口 + * 又广播给 N 个插件,最终放大为 N² 次。这里按“重载方法 + 完整目标签名 + phase”识别, + * 不会把同一方法里的其他 Site 调度或其他目标误判为重复入口。 + */ + private fun detectExistingEntryPhases( + bytes: ByteArray, + targets: List, + ): Map> { + val node = ClassNode(Opcodes.ASM9) + ClassReader(bytes).accept(node, ClassReader.SKIP_DEBUG) + val result = mutableMapOf>() + for (method in node.methods) { + val signatures = targets.asSequence() + .filter { it.target.name == method.name && matchesEntryDescriptor(it.target.descriptor, method.desc) } + .map { it.target.signature } + .toSet() + if (signatures.isEmpty()) continue + for (instruction in method.instructions) { + if (instruction !is MethodInsnNode || + instruction.opcode != Opcodes.INVOKESTATIC || + instruction.owner != BRIDGE_OWNER || + (instruction.name != "dispatch" && instruction.name != "dispatchBypass") + ) continue + val emittedSignature = findEmittedSignature(instruction) ?: continue + val phase = signatures.firstNotNullOfOrNull { entryPhaseOf(it, emittedSignature) } ?: continue + result.computeIfAbsent(MethodKey(method.name, method.desc)) { mutableSetOf() }.add(phase) + } + } + return result + } + + /** 只回看当前 Bridge 调度块;遇到上一个 Bridge 调用即停止,避免跨块取到旧签名。 */ + private fun findEmittedSignature(call: MethodInsnNode): String? { + var cursor = call.previous + var scannedRealInstructions = 0 + while (cursor != null && scannedRealInstructions < MAX_DISPATCH_PREFIX_INSNS) { + if (cursor.opcode >= 0) scannedRealInstructions++ + if (cursor is MethodInsnNode && cursor.owner == BRIDGE_OWNER) return null + if (cursor is LdcInsnNode && cursor.cst is String) return cursor.cst as String + cursor = cursor.previous + } + return null + } + + private fun entryPhaseOf(targetSignature: String, emittedSignature: String): EntryPhase? = when (emittedSignature) { + "$targetSignature@LEAD" -> EntryPhase.LEAD + "$targetSignature@TRAIL", "$targetSignature@TRAIL_THROW" -> EntryPhase.TRAIL + "$targetSignature@SPLICE" -> EntryPhase.SPLICE + else -> null + } + + private fun matchesEntryDescriptor(pattern: String, actual: String): Boolean { + if (pattern == actual || pattern == "(*)" || pattern == "(*)V" || pattern == "()*") return true + val patternClose = pattern.indexOf(')') + val actualClose = actual.indexOf(')') + if (!pattern.startsWith('(') || patternClose < 0 || !actual.startsWith('(') || actualClose < 0) return false + val argsMatch = pattern.substring(1, patternClose) == "*" || + pattern.substring(1, patternClose) == actual.substring(1, actualClose) + val returnMatch = pattern.substring(patternClose + 1) == "*" || + pattern.substring(patternClose + 1) == actual.substring(actualClose + 1) + return argsMatch && returnMatch + } + private class RemapperBridge(val r: NameResolver) : org.objectweb.asm.commons.Remapper() { override fun map(internalName: String): String = r.resolveOwner(internalName) override fun mapMethodName(owner: String, name: String, descriptor: String): String = @@ -317,6 +405,7 @@ class Scalpel( private class WeavingClassVisitor( api: Int, cv: ClassWriter, val targets: List, + val existingEntryPhases: Map>, ) : org.objectweb.asm.ClassVisitor(api, cv) { private lateinit var className: String @@ -333,7 +422,16 @@ class Scalpel( for (s in matching) mergedKinds.addAll(s.kinds) val mergedSpec = AdviceTargetSpec(matching.first().target, mergedKinds, matching.flatMap { it.sites }) Forensics.debug("weave inject $className.$name$descriptor kinds=$mergedKinds static=${(access and Opcodes.ACC_STATIC) != 0}") - return AdviceInjector(api, base, access, name, descriptor, className, mergedSpec) + return AdviceInjector( + api, + base, + access, + name, + descriptor, + className, + mergedSpec, + existingEntryPhases[MethodKey(name, descriptor)].orEmpty(), + ) } private fun matchesDescriptor(pattern: String, actual: String): Boolean { @@ -368,6 +466,7 @@ class Scalpel( val methodName: String, val methodDesc: String, val ownerName: String, val spec: AdviceTargetSpec, + val existingEntryPhases: Set, ) : AdviceAdapter(api, mv, access, methodName, methodDesc) { // 使用用户声明的 target.signature 作为 dispatch key, @@ -378,7 +477,7 @@ class Scalpel( private val entryKinds = spec.kinds - kindsWithSite override fun onMethodEnter() { - if (AdviceKind.LEAD in entryKinds) { + if (AdviceKind.LEAD in entryKinds && EntryPhase.LEAD !in existingEntryPhases) { emitDispatcherCall("@LEAD") pop() } @@ -391,14 +490,14 @@ class Scalpel( it == AdviceKind.SPLICE || it == AdviceKind.EXCISE || it == AdviceKind.BYPASS || it == AdviceKind.GRAFT || it == AdviceKind.TRIM } - if (hasSplicePhase) { + if (hasSplicePhase && EntryPhase.SPLICE !in existingEntryPhases) { emitDispatcherCall("@SPLICE") emitReturnIfNonNull() } } override fun onMethodExit(opcode: Int) { - if (AdviceKind.TRAIL !in entryKinds) return + if (AdviceKind.TRAIL !in entryKinds || EntryPhase.TRAIL in existingEntryPhases) return if (opcode == ATHROW) { emitThrowTrailCall() } else { diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeFieldAccessTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeFieldAccessTest.kt new file mode 100644 index 000000000..c4cd635f4 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/bridge/IncisionBridgeFieldAccessTest.kt @@ -0,0 +1,40 @@ +package taboolib.module.incision.bridge + +import io.izzel.incision.bridge.IncisionBridge +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.objectweb.asm.Type + +/** + * canonical Bridge 私有字段访问回归。 + * + * Side-car body 与宿主不是 nestmate,不能直接访问 private 字段;Bridge 必须在没有 JVMTI + * native 的环境下同时支持实例和静态字段,供多个隔离插件共享同一个稳定入口。 + */ +@DisplayName("IncisionBridge side-car 字段访问") +class IncisionBridgeFieldAccessTest { + + @Test + fun `reads and writes private fields without native backend`() { + val fixture = PrivateFieldFixture() + val owner = PrivateFieldFixture::class.java + + assertEquals(7, IncisionBridge.accessFieldGet(fixture, owner, "value", "I")) + IncisionBridge.accessFieldSet(fixture, owner, "value", "I", 19) + assertEquals(19, IncisionBridge.accessFieldGet(fixture, owner, "value", "I")) + + val staticDescriptor = Type.getDescriptor(String::class.java) + assertEquals("initial", IncisionBridge.accessStaticFieldGet(owner, "shared", staticDescriptor)) + IncisionBridge.accessStaticFieldSet(owner, "shared", staticDescriptor, "changed") + assertEquals("changed", IncisionBridge.accessStaticFieldGet(owner, "shared", staticDescriptor)) + } + + private class PrivateFieldFixture { + private var value: Int = 7 + + companion object { + private var shared: String = "initial" + } + } +} diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt index 705289bf2..f0efb3c3f 100644 --- a/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt +++ b/module/incision/src/test/kotlin/taboolib/module/incision/loader/JvmtiBackendReentrantTest.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger @@ -24,10 +25,12 @@ import java.util.concurrent.atomic.AtomicInteger class JvmtiBackendReentrantTest { @Suppress("UNCHECKED_CAST") - private fun getTransformers(): ConcurrentHashMap ByteArray?>> { + private fun getTransformers(): ConcurrentHashMap ByteArray?>> { val field = JvmtiBackend::class.java.getDeclaredField("transformers") field.isAccessible = true - return field.get(JvmtiBackend) as ConcurrentHashMap ByteArray?>> + // 测试通过反射直接操作生产表,value 类型必须与生产代码完全一致;ArrayList 会在 + // onClassFileLoad 读取字段时触发 erased generic 之后的 CopyOnWriteArrayList 强转失败。 + return field.get(JvmtiBackend) as ConcurrentHashMap ByteArray?>> } private fun getReentrantGuard(): ThreadLocal { @@ -50,7 +53,7 @@ class JvmtiBackendReentrantTest { fun normalTransformerInvocation() { val callCount = AtomicInteger(0) val transformers = getTransformers() - transformers.computeIfAbsent("com/example/TestClass") { mutableListOf() } + transformers.computeIfAbsent("com/example/TestClass") { CopyOnWriteArrayList() } .add { bytes -> callCount.incrementAndGet(); bytes } val result = JvmtiBackend.onClassFileLoad(null, "com/example/TestClass", byteArrayOf(1, 2, 3)) @@ -76,7 +79,7 @@ class JvmtiBackendReentrantTest { val transformers = getTransformers() // transformer B:处理 InnerClass 时反向触发 OuterClass 加载 - transformers.computeIfAbsent("com/example/InnerClass") { mutableListOf() } + transformers.computeIfAbsent("com/example/InnerClass") { CopyOnWriteArrayList() } .add { bytes -> callCountB.incrementAndGet() JvmtiBackend.onClassFileLoad(null, "com/example/OuterClass", byteArrayOf(0xCA.toByte())) @@ -84,7 +87,7 @@ class JvmtiBackendReentrantTest { } // transformer A:处理 OuterClass 时触发 InnerClass 加载 - transformers.computeIfAbsent("com/example/OuterClass") { mutableListOf() } + transformers.computeIfAbsent("com/example/OuterClass") { CopyOnWriteArrayList() } .add { bytes -> callCountA.incrementAndGet() JvmtiBackend.onClassFileLoad(null, "com/example/InnerClass", byteArrayOf(0xFE.toByte())) @@ -106,7 +109,7 @@ class JvmtiBackendReentrantTest { val callCount = AtomicInteger(0) val transformers = getTransformers() - transformers.computeIfAbsent("com/example/SelfRef") { mutableListOf() } + transformers.computeIfAbsent("com/example/SelfRef") { CopyOnWriteArrayList() } .add { bytes -> val depth = callCount.incrementAndGet() if (depth > 100) { @@ -135,7 +138,7 @@ class JvmtiBackendReentrantTest { "com/example/B" -> "com/example/C" else -> "com/example/A" } - transformers.computeIfAbsent(cls) { mutableListOf() } + transformers.computeIfAbsent(cls) { CopyOnWriteArrayList() } .add { bytes -> counts[cls]!!.incrementAndGet() JvmtiBackend.onClassFileLoad(null, nextCls, byteArrayOf(0)) @@ -159,7 +162,7 @@ class JvmtiBackendReentrantTest { val latch = CountDownLatch(1) val transformers = getTransformers() - transformers.computeIfAbsent("com/example/SharedClass") { mutableListOf() } + transformers.computeIfAbsent("com/example/SharedClass") { CopyOnWriteArrayList() } .add { bytes -> threadBCount.incrementAndGet(); bytes } // 线程 A:手动设置重入标记模拟正在 weave @@ -191,7 +194,7 @@ class JvmtiBackendReentrantTest { @DisplayName("transformer 抛异常后重入标记正确清除,后续调用不受影响") fun reentrantGuardClearedAfterException() { val transformers = getTransformers() - transformers.computeIfAbsent("com/example/ErrorClass") { mutableListOf() } + transformers.computeIfAbsent("com/example/ErrorClass") { CopyOnWriteArrayList() } .add { throw RuntimeException("模拟异常") } // 第一次:异常 diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/pred/PredCompilerClassLoaderTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/pred/PredCompilerClassLoaderTest.kt new file mode 100644 index 000000000..5601f6f69 --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/pred/PredCompilerClassLoaderTest.kt @@ -0,0 +1,52 @@ +package taboolib.module.incision.pred + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotSame +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * 谓词生成类的 ClassLoader 隔离回归。 + * + * 两个插件各自拥有 defining loader 时,谓词必须在各自的子 loader 中独立定义,且整个过程 + * 不依赖反射开放 ClassLoader#defineClass,也不允许触发只能由一个插件加载的 JVMTI native。 + */ +@DisplayName("PredCompiler 多 ClassLoader 类定义") +class PredCompilerClassLoaderTest { + + @Test + fun `predicates compile independently under two plugin loaders`() { + val moduleLoader = PredCompilerClassLoaderTest::class.java.classLoader + val pluginLoaderA = object : ClassLoader(moduleLoader) {} + val pluginLoaderB = object : ClassLoader(moduleLoader) {} + + val predicateA = PredCompiler.compile( + "args[0] == \"alpha\"", + AdviceCtx("plugin-a", pluginLoaderA), + ) + val predicateB = PredCompiler.compile( + "args[0] == \"beta\"", + AdviceCtx("plugin-b", pluginLoaderB), + ) + + assertSame(pluginLoaderA, predicateA.javaClass.classLoader.parent) + assertSame(pluginLoaderB, predicateB.javaClass.classLoader.parent) + assertNotSame(predicateA.javaClass.classLoader, predicateB.javaClass.classLoader) + assertTrue(predicateA.test(TestEvalContext(arrayOf("alpha")))) + assertFalse(predicateA.test(TestEvalContext(arrayOf("beta")))) + assertTrue(predicateB.test(TestEvalContext(arrayOf("beta")))) + } + + /** 最小运行时上下文,仅暴露本测试表达式需要的 args。 */ + private class TestEvalContext(private val args: Array) : EvalContext { + override fun argAt(i: Int): Any? = args[i] + override fun argCount(): Int = args.size + override fun thisRef(): Any? = null + override fun result(): Any? = null + override fun env(): Map = emptyMap() + override fun site(): Any? = null + override fun caller(): Any? = null + } +} diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt index e0787c067..9947935b7 100644 --- a/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt +++ b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/BodiesClassGeneratorTest.kt @@ -1,6 +1,7 @@ package taboolib.module.incision.weaver import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -28,12 +29,10 @@ import org.objectweb.asm.tree.MethodInsnNode * * 覆盖两个曾导致 `XXX$$IncisionBodies` 在 defineClass 阶段抛 ClassFormatError 的缺陷: * - * Bug 1(Illegal class name): - * JvmtiBackend 的内部名曾以 `const val "taboolib/module/incision/loader/JvmtiBackend"` - * 硬编码。const 会被内联,TabooLib Shadow relocation 把其中的 `taboolib` 段按「包名(点号)」 - * 规则替换为重定位前缀,当宿主插件 group 较长(多段点号)时得到点斜混合的非法内部名 - * `group.taboolib/module/incision/loader/JvmtiBackend`,被用作 INVOKESTATIC owner 后 - * defineClass 抛 `Illegal class name`。修复:改为运行期 `JvmtiBackend::class.java.name.replace('.', '/')`。 + * Bug 1(跨插件 native 绑定): + * side-car 曾直接调用各插件副本的 JvmtiBackend.nFieldGet。native image 只能绑定一个 + * ClassLoader,第二个插件会在 proceed() 时抛 UnsatisfiedLinkError。修复:private 字段访问 + * 统一调用 bootstrap/system ClassLoader 中不重定位的 IncisionBridge 反射入口。 * * Bug 2(Illegal method name): * 当被 @Splice 的目标方法是构造函数 `` / 静态初始化 `` 时, @@ -154,12 +153,11 @@ class BodiesClassGeneratorTest { } } - // ===== Bug 1:JvmtiBackend 引用必须为合法全斜杠内部名 ===== + // ===== Bug 1:side-car 私有字段访问必须经过 canonical Bridge ===== @Test - @DisplayName("body 中对 JvmtiBackend 的 INVOKESTATIC owner 为合法全斜杠内部名(无点斜混合)") - fun jvmtiBackendOwnerIsValidInternalName() { - // getValue 含 GETFIELD private 字段 → 会改写为 JvmtiBackend.nFieldGet 调用 + @DisplayName("body 中 private GETFIELD 只调用 canonical IncisionBridge") + fun privateFieldUsesCanonicalAccessBridge() { val bytes = generate( buildSampleTargetBytes(), targetMethods = setOf("getValue" to "()I") @@ -167,36 +165,33 @@ class BodiesClassGeneratorTest { assertNotNull(bytes) val node = readNode(bytes!!) - val jvmtiInvokes = node.methods + val accessInvokes = node.methods .flatMap { it.instructions.toArray().toList() } .filterIsInstance() - .filter { it.owner.endsWith("/JvmtiBackend") || it.owner.endsWith(".JvmtiBackend") } - - assertTrue(jvmtiInvokes.isNotEmpty(), "getValue 改写后应至少有一处对 JvmtiBackend 的调用") - - jvmtiInvokes.forEach { insn -> - val owner = insn.owner - // 合法内部名:全斜杠、不含点号、不含点斜混合 - assertFalse(owner.contains('.'), "JvmtiBackend owner 不应含点号(点斜混合非法名): $owner") - assertTrue( - owner.endsWith("incision/loader/JvmtiBackend"), - "JvmtiBackend owner 应为全斜杠内部名,实际: $owner" - ) - } + .filter { it.name == "accessFieldGet" } + + assertEquals(1, accessInvokes.size, "getValue 应产生一个字段访问 Bridge 调用") + assertEquals("io/izzel/incision/bridge/IncisionBridge", accessInvokes.single().owner) } @Test - @DisplayName("含 private 字段访问的 body 改写后整体可加载(间接校验 JvmtiBackend 引用合法)") - fun bodyWithPrivateFieldAccessIsLoadable() { - val bytes = generate( - buildSampleTargetBytes(), + @DisplayName("不加载 JVMTI 时 private 字段 body 可实际执行") + fun bodyWithPrivateFieldAccessExecutesWithoutJvmti() { + val targetBytes = buildSampleTargetBytes() + val bodyBytes = generate( + targetBytes, targetMethods = setOf("getValue" to "()I") ) - assertNotNull(bytes) - val node = readNode(bytes!!) - assertDoesNotThrow { - ByteClassLoader().define(node.name.replace('/', '.'), bytes) - } + assertNotNull(bodyBytes) + val loader = ByteClassLoader() + val targetClass = loader.define(ownerInternal.replace('/', '.'), targetBytes) + val bodyClass = loader.define(readNode(bodyBytes!!).name.replace('/', '.'), bodyBytes) + val target = targetClass.getConstructor(Int::class.javaPrimitiveType).newInstance(37) + + val result = bodyClass.getMethod("getValue\$body", Any::class.java, Array::class.java) + .invoke(null, target, emptyArray()) + + assertEquals(37, result) } // ===== 行为基线:静态方法目标被忽略、空目标返回 null ===== diff --git a/module/incision/src/test/kotlin/taboolib/module/incision/weaver/ScalpelEntryIdempotencyTest.kt b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/ScalpelEntryIdempotencyTest.kt new file mode 100644 index 000000000..1ebbc608c --- /dev/null +++ b/module/incision/src/test/kotlin/taboolib/module/incision/weaver/ScalpelEntryIdempotencyTest.kt @@ -0,0 +1,72 @@ +package taboolib.module.incision.weaver + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodInsnNode +import taboolib.module.incision.api.MethodCoordinate +import taboolib.module.incision.remap.NoopResolver +import taboolib.module.incision.runtime.AdviceKind + +/** + * 多插件 transformer 串联时的入口幂等回归。 + * + * 第二个插件拿到第一个插件已经变换过的字节码;它仍要注册自己的 dispatcher, + * 但不能再向相同方法写入第二个物理 Bridge 入口,否则两个 dispatcher 都会被执行两次。 + */ +@DisplayName("Scalpel 跨插件 Bridge 入口幂等") +class ScalpelEntryIdempotencyTest { + + private val owner = "test/incision/SharedTarget" + private val target = MethodCoordinate(owner, "value", "()I") + + @Test + fun `second transformer reuses existing lead entry`() { + val firstPluginWeaver = weaver() + val secondPluginWeaver = weaver() + + val firstOutput = firstPluginWeaver.weave(targetBytes()) + val secondOutput = secondPluginWeaver.weave(firstOutput) + + assertEquals(1, bridgeDispatchCount(secondOutput)) + } + + /** 两个实例模拟两个隔离插件各自持有的 Scalpel transformer。 */ + private fun weaver(): Scalpel = Scalpel( + resolver = NoopResolver, + targetsByOwner = mapOf( + owner to listOf(Scalpel.AdviceTargetSpec(target, setOf(AdviceKind.LEAD))) + ), + ) + + private fun targetBytes(): ByteArray { + val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, owner, null, "java/lang/Object", null) + writer.visitMethod(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, "value", "()I", null, null).apply { + visitCode() + visitIntInsn(Opcodes.BIPUSH, 20) + visitInsn(Opcodes.IRETURN) + visitMaxs(0, 0) + visitEnd() + } + writer.visitEnd() + return writer.toByteArray() + } + + private fun bridgeDispatchCount(bytes: ByteArray): Int { + val node = ClassNode(Opcodes.ASM9) + ClassReader(bytes).accept(node, 0) + return node.methods + .single { it.name == "value" && it.desc == "()I" } + .instructions + .count { + it is MethodInsnNode && + it.owner == "io/izzel/incision/bridge/IncisionBridge" && + it.name == "dispatch" + } + } +}