diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b865f9..c22aa56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 +## [2.2.3] - 2026-10-03 + +- Unity 可独立通过 Git URL 安装,必装依赖仅保留 UniTask 和官方 Mono.Cecil。 +- Shared Runtime 与 Shared CodeGen 改为可选集成,由包版本宏自动启用。 +- 内置独立特性订阅织入器,支持实例、静态、UniTask 和 MonoBehaviour 生命周期绑定;安装 Shared CodeGen 后由共享生成器接管。 +- Unity 主线程调度契约改为 `IShrinkEventBusMainThreadDispatcher`;共享接口通过 `ShrinkSharedMainThreadDispatcher` 适配,.NET/Godot 接口不变。 +- 独立验证工程取消私有 registry 要求,增加外部消费程序集的生成绑定测试及可选调度适配测试。 + ## [2.0.1] - 2026-08-28 ### Added diff --git a/CodeGen/Editor/EventBusAssemblyWeaver.cs b/CodeGen/Editor/EventBusAssemblyWeaver.cs new file mode 100644 index 0000000..6a16545 --- /dev/null +++ b/CodeGen/Editor/EventBusAssemblyWeaver.cs @@ -0,0 +1,507 @@ +#nullable enable +#if !SHRINK_EVENTBUS_HAS_SHARED_CODEGEN +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace ShrinkEventBus.CodeGen +{ +internal static class EventBusAssemblyWeaver +{ + private const string EventRuntime = "ShrinkEventBus.Runtime"; + private const string Marker = "ShrinkEventBus.Generated.StandaloneWeaveMarker"; + + public static bool Weave(ModuleDefinition module) + { + if (!References(module, EventRuntime) || module.GetType(Marker) != null) + return false; + var instances = 0; + var statics = 0; + WeaveEventBus(module, ref instances, ref statics); + if (instances == 0 && statics == 0) + return false; + module.Types.Add(new TypeDefinition("ShrinkEventBus.Generated", "StandaloneWeaveMarker", + TypeAttributes.NotPublic | TypeAttributes.Abstract | TypeAttributes.Sealed, module.TypeSystem.Object)); + return true; + } + + private static void WeaveEventBus(ModuleDefinition module, + ref int instanceCount, ref int staticCount) + { + var subscriberAttribute = RequireType(module, "ShrinkEventBus.ShrinkEventSubscriberAttribute", EventRuntime); + var subscribeAttribute = RequireType(module, "ShrinkEventBus.ShrinkSubscribeAttribute", EventRuntime); + var allTypes = AllTypes(module.Types).Where(type => !type.IsInterface).ToArray(); + foreach (var type in allTypes.Where(type => HasAttribute(type, subscriberAttribute))) + { + var instanceHandlers = type.Methods.Where(method => !method.IsStatic && HasAttribute(method, subscribeAttribute)).ToArray(); + if (instanceHandlers.Length > 0) + { + var lifetime = ReadInt(type.CustomAttributes.First(a => a.AttributeType.FullName == subscriberAttribute.FullName), "Lifetime", 0); + if (InjectInstanceBinding(module, type, instanceHandlers, subscribeAttribute)) + instanceCount++; + if (lifetime == 1) + InjectAwakeToDestroyLifetime(type, module); + else if (lifetime != 0) + throw new InvalidOperationException($"{type.FullName}: unsupported subscriber lifetime."); + } + } + + var staticTypes = allTypes.Where(type => HasAttribute(type, subscriberAttribute)) + .Where(type => type.Methods.Any(method => method.IsStatic && HasAttribute(method, subscribeAttribute))) + .ToArray(); + if (staticTypes.Length > 0) + { + staticCount = staticTypes.Sum(type => type.Methods.Count(method => method.IsStatic && HasAttribute(method, subscribeAttribute))); + InjectStaticBootstrap(module, staticTypes, subscriberAttribute, subscribeAttribute); + } + } + + private static void InjectAwakeToDestroyLifetime(TypeDefinition type, ModuleDefinition module) + { + if (!InheritsFrom(type, "UnityEngine.MonoBehaviour")) + throw new InvalidOperationException($"[ShrinkEventSubscriber(Lifetime = AwakeToDestroy)] requires MonoBehaviour: {type.FullName}."); + + const string bindingFieldName = "__shrinkEventBusAwakeToDestroyBinding"; + if (type.Fields.Any(field => field.Name == bindingFieldName)) + throw new InvalidOperationException($"Reserved generated field already exists on {type.FullName}: {bindingFieldName}."); + + var disposableType = new TypeReference("System", "IDisposable", module, module.TypeSystem.CoreLibrary); + var bindingField = new FieldDefinition(bindingFieldName, FieldAttributes.Private, disposableType); + type.Fields.Add(bindingField); + var eventBusType = RequireType(module, "ShrinkEventBus.EventBus", EventRuntime); + var attachMethod = ImportMethod(module, eventBusType, + method => method.Name == "Attach" && method.IsStatic && method.Parameters.Count == 2); + var disposeMethod = new MethodReference("Dispose", module.TypeSystem.Void, disposableType) { HasThis = true }; + InjectAwake(type, module, bindingField, attachMethod); + InjectOnDestroy(type, module, bindingField, disposeMethod); + } + + private static void InjectAwake(TypeDefinition type, ModuleDefinition module, + FieldDefinition bindingField, MethodReference attachMethod) + { + var awake = type.Methods.FirstOrDefault(method => + method.Name == "Awake" && !method.IsStatic && method.Parameters.Count == 0); + if (awake != null) + { + InsertAttachAtStart(awake, module, bindingField, attachMethod); + return; + } + + var baseAwake = FindBaseMethodReference(type, "Awake", module); + awake = new MethodDefinition("Awake", + baseAwake != null + ? MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual + : MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot, + module.TypeSystem.Void); + awake.Body.InitLocals = true; + var il = awake.Body.GetILProcessor(); + if (baseAwake != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, baseAwake); + } + EmitAttach(il, awake, module, bindingField, attachMethod); + il.Emit(OpCodes.Ret); + type.Methods.Add(awake); + } + + private static void InjectOnDestroy(TypeDefinition type, ModuleDefinition module, + FieldDefinition bindingField, MethodReference disposeMethod) + { + var onDestroy = type.Methods.FirstOrDefault(method => + method.Name == "OnDestroy" && !method.IsStatic && method.Parameters.Count == 0); + if (onDestroy != null) + { + InsertDisposeAtStart(onDestroy, bindingField, disposeMethod); + return; + } + + var baseOnDestroy = FindBaseMethodReference(type, "OnDestroy", module); + onDestroy = new MethodDefinition("OnDestroy", + baseOnDestroy != null + ? MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual + : MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot, + module.TypeSystem.Void); + var il = onDestroy.Body.GetILProcessor(); + EmitDispose(il, bindingField, disposeMethod); + if (baseOnDestroy != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, baseOnDestroy); + } + il.Emit(OpCodes.Ret); + type.Methods.Add(onDestroy); + } + + private static void InsertAttachAtStart(MethodDefinition method, ModuleDefinition module, + FieldDefinition bindingField, MethodReference attachMethod) + { + if (!method.HasBody || method.Body.Instructions.Count == 0) + throw new InvalidOperationException($"Awake has no body: {method.FullName}."); + method.Body.InitLocals = true; + var processor = method.Body.GetILProcessor(); + var instructions = BuildAttachInstructions(processor, method, module, bindingField, attachMethod); + var first = method.Body.Instructions[0]; + foreach (var instruction in instructions) + processor.InsertBefore(first, instruction); + } + + private static void EmitAttach(ILProcessor il, MethodDefinition method, ModuleDefinition module, + FieldDefinition bindingField, MethodReference attachMethod) + { + foreach (var instruction in BuildAttachInstructions(il, method, module, bindingField, attachMethod)) + il.Append(instruction); + } + + private static IReadOnlyList BuildAttachInstructions(ILProcessor il, + MethodDefinition method, ModuleDefinition module, FieldDefinition bindingField, + MethodReference attachMethod) + { + var defaultBusType = module.ImportReference(attachMethod.Parameters[1].ParameterType); + var defaultBus = new VariableDefinition(defaultBusType); + method.Body.Variables.Add(defaultBus); + var attached = il.Create(OpCodes.Nop); + return new[] + { + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldfld, bindingField), + il.Create(OpCodes.Brtrue_S, attached), + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldloca_S, defaultBus), + il.Create(OpCodes.Initobj, defaultBusType), + il.Create(OpCodes.Ldloc, defaultBus), + il.Create(OpCodes.Call, attachMethod), + il.Create(OpCodes.Stfld, bindingField), + attached + }; + } + + private static void InsertDisposeAtStart(MethodDefinition method, + FieldDefinition bindingField, MethodReference disposeMethod) + { + if (!method.HasBody || method.Body.Instructions.Count == 0) + throw new InvalidOperationException($"OnDestroy has no body: {method.FullName}."); + var processor = method.Body.GetILProcessor(); + var instructions = BuildDisposeInstructions(processor, bindingField, disposeMethod); + var first = method.Body.Instructions[0]; + foreach (var instruction in instructions) + processor.InsertBefore(first, instruction); + } + + private static void EmitDispose(ILProcessor il, FieldDefinition bindingField, + MethodReference disposeMethod) + { + foreach (var instruction in BuildDisposeInstructions(il, bindingField, disposeMethod)) + il.Append(instruction); + } + + private static IReadOnlyList BuildDisposeInstructions(ILProcessor il, + FieldDefinition bindingField, MethodReference disposeMethod) + { + var disposed = il.Create(OpCodes.Nop); + return new[] + { + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldfld, bindingField), + il.Create(OpCodes.Brfalse_S, disposed), + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldfld, bindingField), + il.Create(OpCodes.Callvirt, disposeMethod), + il.Create(OpCodes.Ldarg_0), + il.Create(OpCodes.Ldnull), + il.Create(OpCodes.Stfld, bindingField), + disposed + }; + } + + private static MethodReference? FindBaseMethodReference(TypeDefinition type, + string methodName, ModuleDefinition module) + { + try + { + var baseTypeReference = type.BaseType; + while (baseTypeReference != null) + { + var baseType = baseTypeReference.Resolve(); + if (baseType == null) break; + var method = baseType.Methods.FirstOrDefault(candidate => + candidate.Name == methodName && !candidate.IsStatic && candidate.IsVirtual && + candidate.Parameters.Count == 0); + if (method != null) + { + if (baseTypeReference is GenericInstanceType genericBase) + { + return new MethodReference(method.Name, module.ImportReference(method.ReturnType), + module.ImportReference(genericBase)) + { + HasThis = method.HasThis, + ExplicitThis = method.ExplicitThis, + CallingConvention = method.CallingConvention + }; + } + return module.ImportReference(method); + } + baseTypeReference = baseType.BaseType; + } + } + catch { } + return null; + } + + private static bool InheritsFrom(TypeDefinition type, string expectedFullName) + { + var current = type.BaseType; + while (current != null) + { + if (current.FullName == expectedFullName) return true; + try { current = current.Resolve()?.BaseType; } + catch { return false; } + } + return false; + } + + private static bool InjectInstanceBinding(ModuleDefinition module, TypeDefinition owner, + IReadOnlyList handlers, TypeReference subscribeAttribute) + { + var generatedInterface = RequireType(module, "ShrinkEventBus.IShrinkGeneratedSubscriber", EventRuntime); + if (owner.Interfaces.Any(item => item.InterfaceType.FullName == generatedInterface.FullName)) + return false; + var resolverType = RequireType(module, "ShrinkEventBus.IShrinkBusResolver", EventRuntime); + var busKeyType = RequireType(module, "ShrinkEventBus.ShrinkBusKey", EventRuntime); + var bindingType = RequireType(module, "ShrinkEventBus.ShrinkEventBinding", EventRuntime); + var helperType = RequireType(module, "ShrinkEventBus.ShrinkGeneratedBinding", EventRuntime); + var disposableType = RequireType(module, "System.IDisposable", module.TypeSystem.CoreLibrary.Name); + var nullableBusKey = GenericType(module, "System.Nullable`1", true, busKeyType); + var bindingCtor = ImportMethod(module, bindingType, method => method.IsConstructor && method.Parameters.Count == 0); + var bindingAdd = ImportMethod(module, bindingType, method => method.Name == "Add" && method.Parameters.Count == 1); + var interfaceMethod = ImportMethod(module, generatedInterface, method => method.Name == "AttachGenerated"); + var syncSubscribe = ImportMethod(module, helperType, method => method.Name == "Subscribe" && method.HasGenericParameters); + var asyncSubscribe = ImportMethod(module, helperType, method => method.Name == "SubscribeAsync" && method.HasGenericParameters); + var legacySubscribe = ImportMethod(module, helperType, method => method.Name == "SubscribeAsyncLegacy" && method.HasGenericParameters); + var asyncHandlerType = RequireType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", EventRuntime); + + var generated = new MethodDefinition("ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated", + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, + disposableType); + generated.Parameters.Add(new ParameterDefinition("resolver", ParameterAttributes.None, resolverType)); + generated.Parameters.Add(new ParameterDefinition("defaultBus", ParameterAttributes.Optional, nullableBusKey)); + generated.Overrides.Add(interfaceMethod); + generated.Body.InitLocals = true; + var bindingLocal = new VariableDefinition(bindingType); + generated.Body.Variables.Add(bindingLocal); + var il = generated.Body.GetILProcessor(); + il.Emit(OpCodes.Newobj, bindingCtor); + il.Emit(OpCodes.Stloc, bindingLocal); + var classDefaultBus = ReadString(owner.CustomAttributes.First(a => a.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"), "DefaultBus"); + foreach (var handler in handlers) + EmitInstanceSubscription(module, il, handler, handler.CustomAttributes.First(a => a.AttributeType.FullName == subscribeAttribute.FullName), classDefaultBus, + bindingLocal, bindingAdd, syncSubscribe, asyncSubscribe, legacySubscribe, asyncHandlerType); + il.Emit(OpCodes.Ldloc, bindingLocal); + il.Emit(OpCodes.Ret); + owner.Interfaces.Add(new InterfaceImplementation(generatedInterface)); + owner.Methods.Add(generated); + return true; + } + + private static void EmitInstanceSubscription(ModuleDefinition module, ILProcessor il, MethodDefinition handler, + CustomAttribute attribute, string classDefaultBus, VariableDefinition bindingLocal, MethodReference bindingAdd, + MethodReference syncSubscribe, MethodReference asyncSubscribe, MethodReference legacySubscribe, TypeReference asyncHandlerType) + { + var signature = ResolveEventHandler(module, handler, syncSubscribe, asyncSubscribe, legacySubscribe, asyncHandlerType); + var bus = ReadString(attribute, "Bus"); + if (string.IsNullOrWhiteSpace(bus)) bus = classDefaultBus; + il.Emit(OpCodes.Ldloc, bindingLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldstr, bus ?? string.Empty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldftn, module.ImportReference(handler)); + il.Emit(OpCodes.Newobj, DelegateConstructor(module, signature.DelegateType)); + EmitOptions(il, attribute); + il.Emit(OpCodes.Call, signature.Method); + il.Emit(OpCodes.Callvirt, bindingAdd); + } + + private static void InjectStaticBootstrap(ModuleDefinition module, IReadOnlyList types, + TypeReference subscriberAttribute, TypeReference subscribeAttribute) + { + if (module.Types.Any(type => type.FullName == "ShrinkEventBus.Generated.ShrinkGeneratedStaticBindings")) + return; + var registry = RequireType(module, "ShrinkEventBus.ShrinkStaticBindingRegistry", EventRuntime); + var syncRegister = ImportMethod(module, registry, method => method.Name == "Register" && method.HasGenericParameters); + var asyncRegister = ImportMethod(module, registry, method => method.Name == "RegisterAsync" && method.HasGenericParameters); + var legacyRegister = ImportMethod(module, registry, method => method.Name == "RegisterAsyncLegacy" && method.HasGenericParameters); + var asyncHandlerType = RequireType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", EventRuntime); + var bootstrap = new TypeDefinition("ShrinkEventBus.Generated", "ShrinkGeneratedStaticBindings", + TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic, module.TypeSystem.Object); + module.Types.Add(bootstrap); + var register = new MethodDefinition("Register", MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, module.TypeSystem.Void); + bootstrap.Methods.Add(register); + var il = register.Body.GetILProcessor(); + foreach (var type in types) + { + var classBus = ReadString(type.CustomAttributes.First(a => a.AttributeType.FullName == subscriberAttribute.FullName), "DefaultBus"); + foreach (var handler in type.Methods.Where(method => method.IsStatic && HasAttribute(method, subscribeAttribute)).ToArray()) + { + var attribute = handler.CustomAttributes.First(a => a.AttributeType.FullName == subscribeAttribute.FullName); + var signature = ResolveEventHandler(module, handler, syncRegister, asyncRegister, legacyRegister, asyncHandlerType); + var bus = ReadString(attribute, "Bus"); + if (string.IsNullOrWhiteSpace(bus)) bus = classBus; + var bridge = CreateStaticBridge(module, handler); + il.Emit(OpCodes.Ldstr, bus ?? string.Empty); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldftn, bridge); + il.Emit(OpCodes.Newobj, DelegateConstructor(module, signature.DelegateType)); + EmitOptions(il, attribute); + il.Emit(OpCodes.Call, signature.Method); + } + } + il.Emit(OpCodes.Ret); + var moduleType = module.Types.First(type => type.Name == ""); + var initializer = moduleType.Methods.FirstOrDefault(method => method.Name == ".cctor"); + if (initializer == null) + { + initializer = new MethodDefinition(".cctor", MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig | + MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, module.TypeSystem.Void); + initializer.Body.GetILProcessor().Emit(OpCodes.Ret); + moduleType.Methods.Add(initializer); + } + initializer.Body.GetILProcessor().InsertBefore(initializer.Body.Instructions[0], Instruction.Create(OpCodes.Call, register)); + } + + private static (MethodReference Method, TypeReference DelegateType) ResolveEventHandler(ModuleDefinition module, + MethodDefinition handler, MethodReference sync, MethodReference async, MethodReference legacy, TypeReference asyncHandlerType) + { + if (handler.Parameters.Count is < 1 or > 2) + throw new InvalidOperationException($"{handler.FullName}: [ShrinkSubscribe] requires one event parameter and an optional CancellationToken."); + var eventType = module.ImportReference(handler.Parameters[0].ParameterType); + var eventInterface = RequireType(module, "ShrinkEventBus.IShrinkEvent", EventRuntime); + if (!Implements(eventType, eventInterface.FullName)) + throw new InvalidOperationException($"{handler.FullName}: event parameter must implement IShrinkEvent."); + + MethodReference open; + TypeReference delegateType; + if (handler.ReturnType.MetadataType == MetadataType.Void && handler.Parameters.Count == 1) + { + open = sync; + delegateType = GenericType(module, "System.Action`1", false, eventType); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && handler.Parameters.Count == 1) + { + open = legacy; + delegateType = GenericType(module, "System.Func`2", false, eventType, module.ImportReference(handler.ReturnType)); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && handler.Parameters.Count == 2 && + handler.Parameters[1].ParameterType.FullName == "System.Threading.CancellationToken") + { + open = async; + delegateType = new GenericInstanceType(asyncHandlerType) { GenericArguments = { eventType } }; + } + else + throw new InvalidOperationException($"Unsupported [ShrinkSubscribe] signature: {handler.FullName}."); + var closed = new GenericInstanceMethod(open); + closed.GenericArguments.Add(eventType); + return (closed, delegateType); + } + + private static MethodReference CreateStaticBridge(ModuleDefinition module, MethodDefinition handler) + { + var bridge = new MethodDefinition($"ShrinkEventBus.GeneratedStaticHandler_{handler.MetadataToken.ToInt32():X8}", + MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, module.ImportReference(handler.ReturnType)); + foreach (var parameter in handler.Parameters) + bridge.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType))); + var il = bridge.Body.GetILProcessor(); + foreach (var parameter in bridge.Parameters) il.Emit(OpCodes.Ldarg, parameter); + il.Emit(OpCodes.Call, module.ImportReference(handler)); + il.Emit(OpCodes.Ret); + handler.DeclaringType.Methods.Add(bridge); + return bridge; + } + + private static void EmitOptions(ILProcessor il, CustomAttribute attribute) + { + il.Emit(OpCodes.Ldc_I4, ReadInt(attribute, "Priority", 2)); + il.Emit(OpCodes.Ldc_I4, ReadInt(attribute, "NumericPriority", 0)); + il.Emit(ReadBool(attribute, "ReceiveCanceled", false) ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + } + + private static bool References(ModuleDefinition module, string assemblyName) => + module.AssemblyReferences.Any(reference => string.Equals(reference.Name, assemblyName, StringComparison.Ordinal)); + + private static TypeReference RequireType(ModuleDefinition module, string fullName, string assemblyName) => + FindType(module, fullName, assemblyName) ?? throw new InvalidOperationException($"Required type {fullName} was not found in {assemblyName}."); + + private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName) + { + if (string.Equals(module.Assembly.Name.Name, assemblyName, StringComparison.Ordinal)) + return module.GetType(fullName); + var reference = module.AssemblyReferences.FirstOrDefault(item => string.Equals(item.Name, assemblyName, StringComparison.Ordinal)); + if (reference == null) return null; + var assembly = module.AssemblyResolver.Resolve(reference); + return assembly?.MainModule.GetType(fullName) is { } definition ? module.ImportReference(definition) : null; + } + + private static MethodReference ImportMethod(ModuleDefinition module, TypeReference type, Func predicate) + { + var definition = type.Resolve() ?? throw new InvalidOperationException($"Unable to resolve {type.FullName}."); + var method = definition.Methods.SingleOrDefault(predicate) ?? throw new InvalidOperationException($"Required method was not found on {type.FullName}."); + return module.ImportReference(method); + } + + private static IEnumerable AllTypes(IEnumerable roots) + { + foreach (var type in roots) + { + yield return type; + foreach (var nested in AllTypes(type.NestedTypes)) yield return nested; + } + } + + private static bool HasAttribute(ICustomAttributeProvider provider, TypeReference attribute) => HasAttribute(provider, attribute.FullName); + private static bool HasAttribute(ICustomAttributeProvider provider, string fullName) => provider.CustomAttributes.Any(a => a.AttributeType.FullName == fullName); + + private static bool Implements(TypeReference type, string interfaceFullName) + { + try + { + TypeDefinition? current = type.Resolve(); + while (current != null) + { + if (current.Interfaces.Any(item => item.InterfaceType.FullName == interfaceFullName)) return true; + current = current.BaseType?.Resolve(); + } + } + catch { } + return false; + } + + private static TypeReference GenericType(ModuleDefinition module, string name, bool valueType, params TypeReference[] arguments) + { + var open = new TypeReference("System", name.Substring(name.LastIndexOf('.') + 1), module, module.TypeSystem.CoreLibrary, valueType); + var result = new GenericInstanceType(open); + foreach (var argument in arguments) result.GenericArguments.Add(argument); + return result; + } + + private static MethodReference DelegateConstructor(ModuleDefinition module, TypeReference delegateType) + { + var ctor = new MethodReference(".ctor", module.TypeSystem.Void, delegateType) { HasThis = true }; + ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); + ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.IntPtr)); + return ctor; + } + + private static string ReadString(CustomAttribute attribute, string name) => + attribute.Properties.FirstOrDefault(item => item.Name == name).Argument.Value as string ?? string.Empty; + private static int ReadInt(CustomAttribute attribute, string name, int fallback) => + attribute.Properties.FirstOrDefault(item => item.Name == name) is { Name: not null } item ? Convert.ToInt32(item.Argument.Value) : fallback; + private static bool ReadBool(CustomAttribute attribute, string name, bool fallback) => + attribute.Properties.FirstOrDefault(item => item.Name == name) is { Name: not null } item ? Convert.ToBoolean(item.Argument.Value) : fallback; +} +} + +#endif diff --git a/CodeGen/Editor/EventBusAssemblyWeaver.cs.meta b/CodeGen/Editor/EventBusAssemblyWeaver.cs.meta new file mode 100644 index 0000000..77b44f2 --- /dev/null +++ b/CodeGen/Editor/EventBusAssemblyWeaver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 578fc703a63d4b8e9a414eb8c19a472a diff --git a/CodeGen/Editor/EventBusILPostProcessor.cs b/CodeGen/Editor/EventBusILPostProcessor.cs index b53c511..93478e6 100644 --- a/CodeGen/Editor/EventBusILPostProcessor.cs +++ b/CodeGen/Editor/EventBusILPostProcessor.cs @@ -1,7 +1,10 @@ #nullable enable - +using System; using System.Collections.Generic; -using ShrinkShared.CodeGen; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.ILPostProcessing; @@ -11,12 +14,66 @@ namespace ShrinkEventBus.CodeGen { public override ILPostProcessor GetInstance() => this; - // ShrinkRegistryILPostProcessor owns the complete pass, including event bindings. - public override bool WillProcess(ICompiledAssembly compiledAssembly) => false; + public override bool WillProcess(ICompiledAssembly compiledAssembly) + { +#if SHRINK_EVENTBUS_HAS_SHARED_CODEGEN + // The shared processor owns event weaving when its package is installed. + return false; +#else + return compiledAssembly.Name != "ShrinkEventBus.Runtime" && + compiledAssembly.References.Any(path => + Path.GetFileNameWithoutExtension(path) == "ShrinkEventBus.Runtime"); +#endif + } - public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) => - WillProcess(compiledAssembly) - ? UnityShrinkCodeGenAdapter.Process(compiledAssembly, "ShrinkEventBus.CodeGen") - : new ILPostProcessResult(compiledAssembly.InMemoryAssembly, new List()); + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + var diagnostics = new List(); +#if !SHRINK_EVENTBUS_HAS_SHARED_CODEGEN + if (!WillProcess(compiledAssembly)) + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics); + try + { + using var resolver = new PostProcessorAssemblyResolver(compiledAssembly); + using var pe = new MemoryStream(compiledAssembly.InMemoryAssembly.PeData); + var pdbData = compiledAssembly.InMemoryAssembly.PdbData; + var hasSymbols = pdbData != null && pdbData.Length > 0; + using var pdb = hasSymbols ? new MemoryStream(pdbData!) : null; + using var assembly = AssemblyDefinition.ReadAssembly(pe, new ReaderParameters + { + AssemblyResolver = resolver, + ReadingMode = ReadingMode.Immediate, + ReflectionImporterProvider = new PostProcessorReflectionImporterProvider(), + ReadSymbols = hasSymbols, + SymbolStream = pdb, + SymbolReaderProvider = hasSymbols ? new PortablePdbReaderProvider() : null + }); + resolver.AddAssemblyDefinitionBeingOperatedOn(assembly); + if (assembly.Name.HasPublicKey) + throw new InvalidOperationException("Signed assemblies cannot be woven by the standalone EventBus processor."); + if (!EventBusAssemblyWeaver.Weave(assembly.MainModule)) + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics); + using var outputPe = new MemoryStream(); + using var outputPdb = new MemoryStream(); + assembly.Write(outputPe, new WriterParameters + { + WriteSymbols = hasSymbols, + SymbolStream = hasSymbols ? outputPdb : null, + SymbolWriterProvider = hasSymbols ? new PortablePdbWriterProvider() : null + }); + return new ILPostProcessResult(new InMemoryAssembly(outputPe.ToArray(), + hasSymbols ? outputPdb.ToArray() : Array.Empty()), diagnostics); + } + catch (Exception exception) + { + diagnostics.Add(new DiagnosticMessage + { + DiagnosticType = DiagnosticType.Error, + MessageData = $"[ShrinkEventBus.CodeGen] {exception.Message}" + }); + } +#endif + return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, diagnostics); + } } } diff --git a/CodeGen/Editor/PostProcessorAssemblyResolver.cs b/CodeGen/Editor/PostProcessorAssemblyResolver.cs index 1ef8de1..e22ba04 100644 --- a/CodeGen/Editor/PostProcessorAssemblyResolver.cs +++ b/CodeGen/Editor/PostProcessorAssemblyResolver.cs @@ -102,6 +102,9 @@ namespace ShrinkEventBus.CodeGen public void Dispose() { + foreach (var assembly in _cache.Values) + assembly.Dispose(); + _cache.Clear(); } } } diff --git a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef index 0bd9538..41dda8a 100644 --- a/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef +++ b/CodeGen/Editor/Unity.ShrinkEventBus.CodeGen.asmdef @@ -1,24 +1,29 @@ { - "name": "Unity.ShrinkEventBus.CodeGen", - "rootNamespace": "ShrinkEventBus.CodeGen", - "references": [ - "ShrinkEventBus.Runtime", - "Unity.ShrinkShared.CodeGen" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": true, - "overrideReferences": true, - "precompiledReferences": [ - "Mono.Cecil.dll", - "Mono.Cecil.Mdb.dll", - "Mono.Cecil.Pdb.dll", - "Mono.Cecil.Rocks.dll" - ], - "autoReferenced": false, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false + "name": "Unity.ShrinkEventBus.CodeGen", + "rootNamespace": "ShrinkEventBus.CodeGen", + "references": [ + "ShrinkEventBus.Runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "Mono.Cecil.dll", + "Mono.Cecil.Mdb.dll", + "Mono.Cecil.Pdb.dll", + "Mono.Cecil.Rocks.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.cneicy.shrink-shared-codegen", + "expression": "0.2.1", + "define": "SHRINK_EVENTBUS_HAS_SHARED_CODEGEN" + } + ], + "noEngineReferences": false } diff --git a/Development~/UnityProject/Assets/ConsumerTests/ConsumerTests.cs b/Development~/UnityProject/Assets/ConsumerTests/ConsumerTests.cs new file mode 100644 index 0000000..2497d0d --- /dev/null +++ b/Development~/UnityProject/Assets/ConsumerTests/ConsumerTests.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Threading; +using Cysharp.Threading.Tasks; +using NUnit.Framework; +using ShrinkEventBus; +using UnityEngine.TestTools; + +namespace EventBusConsumer +{ + internal readonly struct Ping : IShrinkEvent { } + + [ShrinkEventSubscriber] + internal sealed class Subscriber + { + public int SyncHits; + public int AsyncHits; + [ShrinkSubscribe] private void OnPing(Ping value) => SyncHits++; + [ShrinkSubscribe] private async UniTask OnPingAsync(Ping value, CancellationToken token) + { + await UniTask.Yield(cancellationToken: token); + AsyncHits++; + } + } + + public sealed class ConsumerTests + { + [UnityTest] + public IEnumerator ConsumerAssemblyGeneratesBindingsAndDisposesThem() + { + using var host = new ShrinkEventBusHost(); + var bus = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline()); + var target = new Subscriber(); + Assert.IsInstanceOf(target); + var binding = host.Attach(target, ShrinkBusKey.Game); + yield return bus.PostAsync(new Ping()).ToCoroutine(); + Assert.AreEqual(1, target.SyncHits); + Assert.AreEqual(1, target.AsyncHits); + binding.Dispose(); + yield return bus.PostAsync(new Ping()).ToCoroutine(); + Assert.AreEqual(1, target.SyncHits); + Assert.AreEqual(1, target.AsyncHits); + } + } +} diff --git a/Development~/UnityProject/Assets/ConsumerTests/EventBus.Consumer.Tests.asmdef b/Development~/UnityProject/Assets/ConsumerTests/EventBus.Consumer.Tests.asmdef new file mode 100644 index 0000000..7adfa0d --- /dev/null +++ b/Development~/UnityProject/Assets/ConsumerTests/EventBus.Consumer.Tests.asmdef @@ -0,0 +1,14 @@ +{ + "name": "EventBus.Consumer.Tests", + "references": [ + "ShrinkEventBus.Runtime", + "UniTask" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "autoReferenced": false +} diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json index fc734ec..cc0e304 100644 --- a/Development~/UnityProject/Packages/manifest.json +++ b/Development~/UnityProject/Packages/manifest.json @@ -1,13 +1,4 @@ { - "scopedRegistries": [ - { - "name": "ShrinkSDK", - "url": "https://git.crash.work/api/packages/ShrinkSDK/npm/", - "scopes": [ - "com.cneicy" - ] - } - ], "dependencies": { "com.unity.test-framework": "1.1.33", "com.cneicy.shrink-eventbus": "file:../../..", diff --git a/Integration.meta b/Integration.meta new file mode 100644 index 0000000..563292e --- /dev/null +++ b/Integration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 27180d887eae401ca4e2547c96534ec4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Integration/Runtime.meta b/Integration/Runtime.meta new file mode 100644 index 0000000..d78d5dc --- /dev/null +++ b/Integration/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eae196d52d4a4091bdf0aa90888b6c97 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef b/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef new file mode 100644 index 0000000..a6eaf21 --- /dev/null +++ b/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef @@ -0,0 +1,19 @@ +{ + "name": "ShrinkEventBus.Integration.Runtime", + "rootNamespace": "ShrinkEventBus.Integration", + "references": [ + "ShrinkEventBus.Runtime", + "ShrinkRuntime.Abstractions" + ], + "autoReferenced": true, + "defineConstraints": [ + "SHRINK_EVENTBUS_HAS_SHARED_RUNTIME" + ], + "versionDefines": [ + { + "name": "com.cneicy.shrink-runtime-abstractions", + "expression": "0.1.0", + "define": "SHRINK_EVENTBUS_HAS_SHARED_RUNTIME" + } + ] +} diff --git a/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef.meta b/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef.meta new file mode 100644 index 0000000..1701194 --- /dev/null +++ b/Integration/Runtime/ShrinkEventBus.Integration.Runtime.asmdef.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 86c48423db2e41e1b208c468296b7faf diff --git a/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs b/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs new file mode 100644 index 0000000..5c62e31 --- /dev/null +++ b/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs @@ -0,0 +1,20 @@ +#if SHRINK_EVENTBUS_HAS_SHARED_RUNTIME +using System; +using ShrinkSDK.Runtime; + +namespace ShrinkEventBus.Integration +{ + public sealed class ShrinkSharedMainThreadDispatcher : IShrinkEventBusMainThreadDispatcher + { + private readonly IShrinkMainThreadDispatcher _dispatcher; + + public ShrinkSharedMainThreadDispatcher(IShrinkMainThreadDispatcher dispatcher) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + public bool IsMainThread => _dispatcher.IsMainThread; + public bool TryPost(Action action) => _dispatcher.TryPost(action); + } +} +#endif diff --git a/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs.meta b/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs.meta new file mode 100644 index 0000000..4c3791f --- /dev/null +++ b/Integration/Runtime/ShrinkSharedMainThreadDispatcher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f818313cb4e3447f9622b71decefa86c diff --git a/Integration/Tests.meta b/Integration/Tests.meta new file mode 100644 index 0000000..ade68c5 --- /dev/null +++ b/Integration/Tests.meta @@ -0,0 +1,5 @@ +fileFormatVersion: 2 +guid: bc17b8a03b8d4abe8068d9310fe12f19 +folderAsset: yes +DefaultImporter: + externalObjects: {} diff --git a/Integration/Tests/SharedDispatcherTests.cs b/Integration/Tests/SharedDispatcherTests.cs new file mode 100644 index 0000000..c42c99f --- /dev/null +++ b/Integration/Tests/SharedDispatcherTests.cs @@ -0,0 +1,43 @@ +#if SHRINK_EVENTBUS_HAS_SHARED_RUNTIME +using System; +using NUnit.Framework; +using ShrinkSDK.Runtime; + +namespace ShrinkEventBus.Integration.Tests +{ + public sealed class SharedDispatcherTests + { + private sealed class Dispatcher : IShrinkMainThreadDispatcher + { + public bool IsMainThread => false; + public Action Pending; + public bool TryPost(Action action) { Pending = action; return true; } + } + + [Test] + public void AdapterPreservesAffinityAndPostsThroughSharedDispatcher() + { + var original = ShrinkEventBusRuntime.MainThreadDispatcher; + try + { + var dispatcher = new Dispatcher(); + var adapter = new ShrinkSharedMainThreadDispatcher(dispatcher); + ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(adapter); + Assert.IsFalse(ShrinkEventBusRuntime.MainThreadDispatcher.IsMainThread); + var hits = 0; + Assert.IsTrue(ShrinkEventBusRuntime.MainThreadDispatcher.TryPost(() => hits++)); + Assert.AreEqual(0, hits); + dispatcher.Pending(); + Assert.AreEqual(1, hits); + } + finally { ShrinkEventBusRuntime.ConfigureMainThreadDispatcher(original); } + } + + [Test] + public void AdapterRejectsNullDispatcher() + { + Assert.Throws(() => new ShrinkSharedMainThreadDispatcher(null)); + } + } +} +#endif diff --git a/Integration/Tests/SharedDispatcherTests.cs.meta b/Integration/Tests/SharedDispatcherTests.cs.meta new file mode 100644 index 0000000..636f109 --- /dev/null +++ b/Integration/Tests/SharedDispatcherTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 87e04bff7dc943dd9a8205b37e631c19 diff --git a/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef b/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef new file mode 100644 index 0000000..183954c --- /dev/null +++ b/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef @@ -0,0 +1,26 @@ +{ + "name": "ShrinkEventBus.Integration.Runtime.Tests", + "references": [ + "ShrinkEventBus.Runtime", + "ShrinkEventBus.Integration.Runtime", + "ShrinkRuntime.Abstractions" + ], + "includePlatforms": [ + "Editor" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS", + "SHRINK_EVENTBUS_HAS_SHARED_RUNTIME" + ], + "versionDefines": [ + { + "name": "com.cneicy.shrink-runtime-abstractions", + "expression": "0.1.0", + "define": "SHRINK_EVENTBUS_HAS_SHARED_RUNTIME" + } + ] +} diff --git a/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef.meta b/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef.meta new file mode 100644 index 0000000..dc89429 --- /dev/null +++ b/Integration/Tests/ShrinkEventBus.Integration.Runtime.Tests.asmdef.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4062d02e94ea4577957d4185688d1454 diff --git a/README.md b/README.md index 9617ca0..673e7e5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,69 @@ -# ShrinkEventBus 2.0 +# ShrinkEventBus ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。 Godot 和普通 .NET 项目安装 `ShrinkSDK.EventBus`;可打包源码位于 `DotNet~`,订阅注册由 `ShrinkSDK.CodeGen` 在构建时织入。 +## Unity 安装 + +要求 Unity 2022.3 或更新版本。在 Package Manager 中使用 **Add package from git URL**,依次安装: + +```text +https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b +https://github.com/cneicy/ShrinkEventBus.git +``` + +第一个地址固定 UniTask 2.5.10。Mono.Cecil 1.11.4 由 Unity 官方 registry 自动解析;无需配置 ShrinkSDK registry。需要锁定 EventBus 版本时,在 Git URL 后添加 `#`。 + +### 可选 ShrinkSDK 集成 + +| 已安装的共享包 | 行为 | +|---|---| +| 无 | 使用包内调度接口和 EventBus 专用 ILPostProcessor | +| Runtime Abstractions 0.1.0+ | 额外启用共享主线程调度器适配程序集 | +| Shared CodeGen 0.2.1+ | 停用包内织入,交给共享 CodeGen 统一生成订阅 | +| 两者都有 | 同时启用上述两项集成 | + +通过 asmdef `versionDefines` 自动生成 `SHRINK_EVENTBUS_HAS_SHARED_RUNTIME` 和 `SHRINK_EVENTBUS_HAS_SHARED_CODEGEN`,不需要手工设置宏。两个共享包由完整工程自行安装,EventBus 不再强制拉取它们。 + +Unity 自定义调度器现在实现 `IShrinkEventBusMainThreadDispatcher`。已有的共享 `IShrinkMainThreadDispatcher` 实现使用以下适配方式: + +```csharp +using ShrinkEventBus; +using ShrinkEventBus.Integration; + +ShrinkEventBusRuntime.ConfigureMainThreadDispatcher( + new ShrinkSharedMainThreadDispatcher(sharedDispatcher)); +``` + +使用自定义 asmdef 的业务程序集还需引用 `ShrinkEventBus.Integration.Runtime`。默认 UniTask PlayerLoop 调度无需配置;请在创建需要自定义调度的 Bus 前完成配置。此调整只影响 Unity,现有 .NET/Godot 配置接口保持不变。 + +### 最小使用示例 + +业务 asmdef 引用 `ShrinkEventBus.Runtime`,使用 UniTask 时再引用 `UniTask`。无需 `partial`、反射注册或手工生成绑定: + +```csharp +using ShrinkEventBus; +using UnityEngine; + +public readonly struct PlayerJoined : IShrinkEvent { } + +[ShrinkEventSubscriber] +public sealed class PlayerHandlers +{ + [ShrinkSubscribe] + private void OnJoined(PlayerJoined value) => Debug.Log("Player joined"); +} + +// 在 Unity 主线程执行;binding 应由实际宿主持有并在生命周期结束时释放。 +using (var binding = EventBus.Attach(new PlayerHandlers())) +{ + EventBus.Post(new PlayerJoined()); +} +``` + +包内 `Development~/UnityProject` 是独立验证工程,包含外部消费程序集的同步/异步订阅测试。使用 Unity Test Runner 运行 EditMode 和 PlayMode 测试;包本身的测试通过 manifest 的 `testables` 开启。 + ## 核心契约 ```csharp @@ -181,7 +241,7 @@ ShrinkEventBusBenchmark.Run( - 外部 Mod DLL 必须携带生成合同;没有合同的生产 DLL 不自动反射注册。 - Editor 诊断是可选观察层,不参与正式无诊断热路径。 -架构审计与来源说明见 `Docs/JustAnyProjectArchitectureAudit.md` 和根目录 `DESIGN.md`。 +Unity 独立安装不包含 Entities/Burst 适配包;需要时由宿主额外安装。 ## 可复用能力 diff --git a/Runtime/IShrinkEventBusMainThreadDispatcher.cs b/Runtime/IShrinkEventBusMainThreadDispatcher.cs new file mode 100644 index 0000000..78ea04a --- /dev/null +++ b/Runtime/IShrinkEventBusMainThreadDispatcher.cs @@ -0,0 +1,12 @@ +#if UNITY_5_3_OR_NEWER +using System; + +namespace ShrinkEventBus +{ + public interface IShrinkEventBusMainThreadDispatcher + { + bool IsMainThread { get; } + bool TryPost(Action action); + } +} +#endif diff --git a/Runtime/IShrinkEventBusMainThreadDispatcher.cs.meta b/Runtime/IShrinkEventBusMainThreadDispatcher.cs.meta new file mode 100644 index 0000000..0924524 --- /dev/null +++ b/Runtime/IShrinkEventBusMainThreadDispatcher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ff0b939fcee4292a6183fd899974c9c diff --git a/Runtime/ShrinkBusSchedulers.cs b/Runtime/ShrinkBusSchedulers.cs index adff912..5435d5b 100644 --- a/Runtime/ShrinkBusSchedulers.cs +++ b/Runtime/ShrinkBusSchedulers.cs @@ -6,7 +6,11 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; -using ShrinkSDK.Runtime; +#if UNITY_5_3_OR_NEWER +using MainThreadDispatcherContract = ShrinkEventBus.IShrinkEventBusMainThreadDispatcher; +#else +using MainThreadDispatcherContract = ShrinkSDK.Runtime.IShrinkMainThreadDispatcher; +#endif namespace ShrinkEventBus { @@ -208,7 +212,7 @@ namespace ShrinkEventBus internal sealed class ShrinkMainThreadScheduler : IShrinkBusScheduler { private readonly ShrinkSchedulerQueue _queue; - private readonly IShrinkMainThreadDispatcher _dispatcher; + private readonly MainThreadDispatcherContract _dispatcher; private int _pumpScheduled; private int _disposed; diff --git a/Runtime/ShrinkEventBus.Runtime.asmdef b/Runtime/ShrinkEventBus.Runtime.asmdef index b80a785..513bd1d 100644 --- a/Runtime/ShrinkEventBus.Runtime.asmdef +++ b/Runtime/ShrinkEventBus.Runtime.asmdef @@ -1,13 +1,12 @@ { - "name": "ShrinkEventBus.Runtime", - "rootNamespace": "ShrinkEventBus", - "references": [ - "UniTask", - "ShrinkRuntime.Abstractions" - ], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "autoReferenced": true + "name": "ShrinkEventBus.Runtime", + "rootNamespace": "ShrinkEventBus", + "references": [ + "UniTask" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "autoReferenced": true } diff --git a/Runtime/ShrinkEventBusRuntime.cs b/Runtime/ShrinkEventBusRuntime.cs index ad7e89b..756c0c6 100644 --- a/Runtime/ShrinkEventBusRuntime.cs +++ b/Runtime/ShrinkEventBusRuntime.cs @@ -2,25 +2,29 @@ using System; using System.Threading; -using ShrinkSDK.Runtime; +#if UNITY_5_3_OR_NEWER +using MainThreadDispatcherContract = ShrinkEventBus.IShrinkEventBusMainThreadDispatcher; +#else +using MainThreadDispatcherContract = ShrinkSDK.Runtime.IShrinkMainThreadDispatcher; +#endif namespace ShrinkEventBus { public static class ShrinkEventBusRuntime { - private static IShrinkMainThreadDispatcher _mainThreadDispatcher = CreateDefaultDispatcher(); + private static MainThreadDispatcherContract _mainThreadDispatcher = CreateDefaultDispatcher(); - public static IShrinkMainThreadDispatcher MainThreadDispatcher => + public static MainThreadDispatcherContract MainThreadDispatcher => Volatile.Read(ref _mainThreadDispatcher); - public static void ConfigureMainThreadDispatcher(IShrinkMainThreadDispatcher dispatcher) + public static void ConfigureMainThreadDispatcher(MainThreadDispatcherContract dispatcher) { if (dispatcher == null) throw new ArgumentNullException(nameof(dispatcher)); Volatile.Write(ref _mainThreadDispatcher, dispatcher); } - private static IShrinkMainThreadDispatcher CreateDefaultDispatcher() + private static MainThreadDispatcherContract CreateDefaultDispatcher() { #if UNITY_5_3_OR_NEWER return new ShrinkUnityMainThreadDispatcher(); @@ -33,7 +37,7 @@ namespace ShrinkEventBus } #if UNITY_5_3_OR_NEWER - internal sealed class ShrinkUnityMainThreadDispatcher : IShrinkMainThreadDispatcher + internal sealed class ShrinkUnityMainThreadDispatcher : MainThreadDispatcherContract { public bool IsMainThread => Cysharp.Threading.Tasks.PlayerLoopHelper.IsMainThread; @@ -48,7 +52,7 @@ namespace ShrinkEventBus } } #else - internal sealed class ShrinkSynchronizationContextDispatcher : IShrinkMainThreadDispatcher + internal sealed class ShrinkSynchronizationContextDispatcher : MainThreadDispatcherContract { private readonly SynchronizationContext? _context; private readonly int _threadId; diff --git a/package.json b/package.json index 4b7e866..d98d4e4 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,15 @@ { "name": "com.cneicy.shrink-eventbus", - "version": "2.2.2", + "version": "2.2.3", "displayName": "ShrinkEventBus", "description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。", "unity": "2022.3", - "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus", - "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/CHANGELOG.md", - "licensesUrl": "https://git.crash.work/ShrinkSDK/ShrinkEventBus/src/branch/main/LICENSE", + "documentationUrl": "https://github.com/cneicy/ShrinkEventBus", + "changelogUrl": "https://github.com/cneicy/ShrinkEventBus/blob/main/CHANGELOG.md", + "licensesUrl": "https://github.com/cneicy/ShrinkEventBus/blob/main/LICENSE", "dependencies": { "com.cysharp.unitask": "2.5.10", - "com.unity.nuget.mono-cecil": "1.11.4", - "com.cneicy.shrink-shared-codegen": "0.2.1", - "com.cneicy.shrink-runtime-abstractions": "0.1.0" + "com.unity.nuget.mono-cecil": "1.11.4" }, "keywords": [ "eventbus", @@ -21,6 +19,6 @@ ], "author": { "name": "cneicy", - "url": "https://git.crash.work/ShrinkSDK" + "url": "https://github.com/cneicy" } }