SyncVarAttributeProcessor.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Mono.CecilX;
  5. using Mono.CecilX.Cil;
  6. using Mono.CecilX.Rocks;
  7. namespace Mirror.Weaver
  8. {
  9. // Processes [SyncVar] attribute fields in NetworkBehaviour
  10. // not static, because ILPostProcessor is multithreaded
  11. public class SyncVarAttributeProcessor
  12. {
  13. // ulong = 64 bytes
  14. const int SyncVarLimit = 64;
  15. AssemblyDefinition assembly;
  16. WeaverTypes weaverTypes;
  17. SyncVarAccessLists syncVarAccessLists;
  18. Logger Log;
  19. string HookParameterMessage(string hookName, TypeReference ValueType) =>
  20. $"void {hookName}({ValueType} oldValue, {ValueType} newValue)";
  21. public SyncVarAttributeProcessor(AssemblyDefinition assembly, WeaverTypes weaverTypes, SyncVarAccessLists syncVarAccessLists, Logger Log)
  22. {
  23. this.assembly = assembly;
  24. this.weaverTypes = weaverTypes;
  25. this.syncVarAccessLists = syncVarAccessLists;
  26. this.Log = Log;
  27. }
  28. // Get hook method if any
  29. public MethodDefinition GetHookMethod(TypeDefinition td, FieldDefinition syncVar, ref bool WeavingFailed)
  30. {
  31. CustomAttribute syncVarAttr = syncVar.GetCustomAttribute<SyncVarAttribute>();
  32. if (syncVarAttr == null)
  33. return null;
  34. string hookFunctionName = syncVarAttr.GetField<string>("hook", null);
  35. if (hookFunctionName == null)
  36. return null;
  37. return FindHookMethod(td, syncVar, hookFunctionName, ref WeavingFailed);
  38. }
  39. // push hook from GetHookMethod() onto the stack as a new Action<T,T>.
  40. // allows for reuse without handling static/virtual cases every time.
  41. public void GenerateNewActionFromHookMethod(FieldDefinition syncVar, ILProcessor worker, MethodDefinition hookMethod)
  42. {
  43. // IL_000a: ldarg.0
  44. // IL_000b: ldftn instance void Mirror.Examples.Tanks.Tank::ExampleHook(int32, int32)
  45. // IL_0011: newobj instance void class [netstandard]System.Action`2<int32, int32>::.ctor(object, native int)
  46. // we support static hook sand instance hooks.
  47. if (hookMethod.IsStatic)
  48. {
  49. // for static hooks, we need to push 'null' first.
  50. // we can't just push nothing.
  51. // stack would get out of balance because we already pushed
  52. // other stuff above.
  53. worker.Emit(OpCodes.Ldnull);
  54. }
  55. else
  56. {
  57. // for instance hooks, we need to push 'this.' first.
  58. worker.Emit(OpCodes.Ldarg_0);
  59. }
  60. MethodReference hookMethodReference;
  61. // if the network behaviour class is generic, we need to make the method reference generic for correct IL
  62. if (hookMethod.DeclaringType.HasGenericParameters)
  63. {
  64. hookMethodReference = hookMethod.MakeHostInstanceGeneric(hookMethod.Module, hookMethod.DeclaringType.MakeGenericInstanceType(hookMethod.DeclaringType.GenericParameters.ToArray()));
  65. }
  66. else
  67. {
  68. hookMethodReference = hookMethod;
  69. }
  70. // we support regular and virtual hook functions.
  71. if (hookMethod.IsVirtual)
  72. {
  73. // for virtual / overwritten hooks, we need different IL.
  74. // this is from simply testing Action = VirtualHook; in C#.
  75. worker.Emit(OpCodes.Dup);
  76. worker.Emit(OpCodes.Ldvirtftn, hookMethodReference);
  77. }
  78. else
  79. {
  80. worker.Emit(OpCodes.Ldftn, hookMethodReference);
  81. }
  82. // call 'new Action<T,T>()' constructor to convert the function to an action
  83. // we need to make an instance of the generic Action<T,T>.
  84. //
  85. // TODO this allocates a new 'Action' for every SyncVar hook call.
  86. // we should allocate it once and store it somewhere in the future.
  87. // hooks are only called on the client though, so it's not too bad for now.
  88. TypeReference actionRef = assembly.MainModule.ImportReference(typeof(Action<,>));
  89. GenericInstanceType genericInstance = actionRef.MakeGenericInstanceType(syncVar.FieldType, syncVar.FieldType);
  90. worker.Emit(OpCodes.Newobj, weaverTypes.ActionT_T.MakeHostInstanceGeneric(assembly.MainModule, genericInstance));
  91. }
  92. MethodDefinition FindHookMethod(TypeDefinition td, FieldDefinition syncVar, string hookFunctionName, ref bool WeavingFailed)
  93. {
  94. List<MethodDefinition> methods = td.GetMethods(hookFunctionName);
  95. List<MethodDefinition> methodsWith2Param = new List<MethodDefinition>(methods.Where(m => m.Parameters.Count == 2));
  96. if (methodsWith2Param.Count == 0)
  97. {
  98. Log.Error($"Could not find hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " +
  99. $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}",
  100. syncVar);
  101. WeavingFailed = true;
  102. return null;
  103. }
  104. foreach (MethodDefinition method in methodsWith2Param)
  105. {
  106. if (MatchesParameters(syncVar, method))
  107. {
  108. return method;
  109. }
  110. }
  111. Log.Error($"Wrong type for Parameter in hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " +
  112. $"Method signature should be {HookParameterMessage(hookFunctionName, syncVar.FieldType)}",
  113. syncVar);
  114. WeavingFailed = true;
  115. return null;
  116. }
  117. bool MatchesParameters(FieldDefinition syncVar, MethodDefinition method)
  118. {
  119. // matches void onValueChange(T oldValue, T newValue)
  120. return method.Parameters[0].ParameterType.FullName == syncVar.FieldType.FullName &&
  121. method.Parameters[1].ParameterType.FullName == syncVar.FieldType.FullName;
  122. }
  123. public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string originalName, FieldDefinition netFieldId)
  124. {
  125. //Create the get method
  126. MethodDefinition get = new MethodDefinition(
  127. $"get_Network{originalName}", MethodAttributes.Public |
  128. MethodAttributes.SpecialName |
  129. MethodAttributes.HideBySig,
  130. fd.FieldType);
  131. ILProcessor worker = get.Body.GetILProcessor();
  132. FieldReference fr;
  133. if (fd.DeclaringType.HasGenericParameters)
  134. {
  135. fr = fd.MakeHostInstanceGeneric();
  136. }
  137. else
  138. {
  139. fr = fd;
  140. }
  141. FieldReference netIdFieldReference = null;
  142. if (netFieldId != null)
  143. {
  144. if (netFieldId.DeclaringType.HasGenericParameters)
  145. {
  146. netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
  147. }
  148. else
  149. {
  150. netIdFieldReference = netFieldId;
  151. }
  152. }
  153. // [SyncVar] GameObject?
  154. if (fd.FieldType.Is<UnityEngine.GameObject>())
  155. {
  156. // return this.GetSyncVarGameObject(ref field, uint netId);
  157. // this.
  158. worker.Emit(OpCodes.Ldarg_0);
  159. worker.Emit(OpCodes.Ldarg_0);
  160. worker.Emit(OpCodes.Ldfld, netIdFieldReference);
  161. worker.Emit(OpCodes.Ldarg_0);
  162. worker.Emit(OpCodes.Ldflda, fr);
  163. worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference);
  164. worker.Emit(OpCodes.Ret);
  165. }
  166. // [SyncVar] NetworkIdentity?
  167. else if (fd.FieldType.Is<NetworkIdentity>())
  168. {
  169. // return this.GetSyncVarNetworkIdentity(ref field, uint netId);
  170. // this.
  171. worker.Emit(OpCodes.Ldarg_0);
  172. worker.Emit(OpCodes.Ldarg_0);
  173. worker.Emit(OpCodes.Ldfld, netIdFieldReference);
  174. worker.Emit(OpCodes.Ldarg_0);
  175. worker.Emit(OpCodes.Ldflda, fr);
  176. worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference);
  177. worker.Emit(OpCodes.Ret);
  178. }
  179. else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
  180. {
  181. // return this.GetSyncVarNetworkBehaviour<T>(ref field, uint netId);
  182. // this.
  183. worker.Emit(OpCodes.Ldarg_0);
  184. worker.Emit(OpCodes.Ldarg_0);
  185. worker.Emit(OpCodes.Ldfld, netIdFieldReference);
  186. worker.Emit(OpCodes.Ldarg_0);
  187. worker.Emit(OpCodes.Ldflda, fr);
  188. MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType);
  189. worker.Emit(OpCodes.Call, getFunc);
  190. worker.Emit(OpCodes.Ret);
  191. }
  192. // [SyncVar] int, string, etc.
  193. else
  194. {
  195. worker.Emit(OpCodes.Ldarg_0);
  196. worker.Emit(OpCodes.Ldfld, fr);
  197. worker.Emit(OpCodes.Ret);
  198. }
  199. get.Body.Variables.Add(new VariableDefinition(fd.FieldType));
  200. get.Body.InitLocals = true;
  201. get.SemanticsAttributes = MethodSemanticsAttributes.Getter;
  202. return get;
  203. }
  204. // for [SyncVar] health, weaver generates
  205. //
  206. // NetworkHealth
  207. // {
  208. // get => health;
  209. // set => GeneratedSyncVarSetter(...)
  210. // }
  211. //
  212. // the setter used to be manually IL generated, but we moved it to C# :)
  213. public MethodDefinition GenerateSyncVarSetter(TypeDefinition td, FieldDefinition fd, string originalName, long dirtyBit, FieldDefinition netFieldId, ref bool WeavingFailed)
  214. {
  215. //Create the set method
  216. MethodDefinition set = new MethodDefinition($"set_Network{originalName}", MethodAttributes.Public |
  217. MethodAttributes.SpecialName |
  218. MethodAttributes.HideBySig,
  219. weaverTypes.Import(typeof(void)));
  220. ILProcessor worker = set.Body.GetILProcessor();
  221. FieldReference fr;
  222. if (fd.DeclaringType.HasGenericParameters)
  223. {
  224. fr = fd.MakeHostInstanceGeneric();
  225. }
  226. else
  227. {
  228. fr = fd;
  229. }
  230. FieldReference netIdFieldReference = null;
  231. if (netFieldId != null)
  232. {
  233. if (netFieldId.DeclaringType.HasGenericParameters)
  234. {
  235. netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
  236. }
  237. else
  238. {
  239. netIdFieldReference = netFieldId;
  240. }
  241. }
  242. // if (!SyncVarEqual(value, ref playerData))
  243. Instruction endOfMethod = worker.Create(OpCodes.Nop);
  244. // NOTE: SyncVar...Equal functions are static.
  245. // don't Emit Ldarg_0 aka 'this'.
  246. // call WeaverSyncVarSetter<T>(T value, ref T field, ulong dirtyBit, Action<T, T> OnChanged = null)
  247. // IL_0000: ldarg.0
  248. // IL_0001: ldarg.1
  249. // IL_0002: ldarg.0
  250. // IL_0003: ldflda int32 Mirror.Examples.Tanks.Tank::health
  251. // IL_0008: ldc.i4.1
  252. // IL_0009: conv.i8
  253. // IL_000a: ldnull
  254. // IL_000b: call instance void [Mirror]Mirror.NetworkBehaviour::GeneratedSyncVarSetter<int32>(!!0, !!0&, uint64, class [netstandard]System.Action`2<!!0, !!0>)
  255. // IL_0010: ret
  256. // 'this.' for the call
  257. worker.Emit(OpCodes.Ldarg_0);
  258. // first push 'value'
  259. worker.Emit(OpCodes.Ldarg_1);
  260. // push 'ref T this.field'
  261. worker.Emit(OpCodes.Ldarg_0);
  262. worker.Emit(OpCodes.Ldflda, fr);
  263. // push the dirty bit for this SyncVar
  264. worker.Emit(OpCodes.Ldc_I8, dirtyBit);
  265. // hook? then push 'new Action<T,T>(Hook)' onto stack
  266. MethodDefinition hookMethod = GetHookMethod(td, fd, ref WeavingFailed);
  267. if (hookMethod != null)
  268. {
  269. GenerateNewActionFromHookMethod(fd, worker, hookMethod);
  270. }
  271. // otherwise push 'null' as hook
  272. else
  273. {
  274. worker.Emit(OpCodes.Ldnull);
  275. }
  276. // call GeneratedSyncVarSetter<T>.
  277. // special cases for GameObject/NetworkIdentity/NetworkBehaviour
  278. // passing netId too for persistence.
  279. if (fd.FieldType.Is<UnityEngine.GameObject>())
  280. {
  281. // GameObject setter needs one more parameter: netId field ref
  282. worker.Emit(OpCodes.Ldarg_0);
  283. worker.Emit(OpCodes.Ldflda, netIdFieldReference);
  284. worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_GameObject);
  285. }
  286. else if (fd.FieldType.Is<NetworkIdentity>())
  287. {
  288. // NetworkIdentity setter needs one more parameter: netId field ref
  289. worker.Emit(OpCodes.Ldarg_0);
  290. worker.Emit(OpCodes.Ldflda, netIdFieldReference);
  291. worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarSetter_NetworkIdentity);
  292. }
  293. // TODO this only uses the persistent netId for types DERIVED FROM NB.
  294. // not if the type is just 'NetworkBehaviour'.
  295. // this is what original implementation did too. fix it after.
  296. else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
  297. {
  298. // NetworkIdentity setter needs one more parameter: netId field ref
  299. // (actually its a NetworkBehaviourSyncVar type)
  300. worker.Emit(OpCodes.Ldarg_0);
  301. worker.Emit(OpCodes.Ldflda, netIdFieldReference);
  302. // make generic version of GeneratedSyncVarSetter_NetworkBehaviour<T>
  303. MethodReference getFunc = weaverTypes.generatedSyncVarSetter_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, fd.FieldType);
  304. worker.Emit(OpCodes.Call, getFunc);
  305. }
  306. else
  307. {
  308. // make generic version of GeneratedSyncVarSetter<T>
  309. MethodReference generic = weaverTypes.generatedSyncVarSetter.MakeGeneric(assembly.MainModule, fd.FieldType);
  310. worker.Emit(OpCodes.Call, generic);
  311. }
  312. worker.Append(endOfMethod);
  313. worker.Emit(OpCodes.Ret);
  314. set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.In, fd.FieldType));
  315. set.SemanticsAttributes = MethodSemanticsAttributes.Setter;
  316. return set;
  317. }
  318. public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds, long dirtyBit, ref bool WeavingFailed)
  319. {
  320. string originalName = fd.Name;
  321. // GameObject/NetworkIdentity SyncVars have a new field for netId
  322. FieldDefinition netIdField = null;
  323. // NetworkBehaviour has different field type than other NetworkIdentityFields
  324. if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>())
  325. {
  326. netIdField = new FieldDefinition($"___{fd.Name}NetId",
  327. FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
  328. weaverTypes.Import<NetworkBehaviour.NetworkBehaviourSyncVar>());
  329. netIdField.DeclaringType = td;
  330. syncVarNetIds[fd] = netIdField;
  331. }
  332. else if (fd.FieldType.IsNetworkIdentityField())
  333. {
  334. netIdField = new FieldDefinition($"___{fd.Name}NetId",
  335. FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
  336. weaverTypes.Import<uint>());
  337. netIdField.DeclaringType = td;
  338. syncVarNetIds[fd] = netIdField;
  339. }
  340. MethodDefinition get = GenerateSyncVarGetter(fd, originalName, netIdField);
  341. MethodDefinition set = GenerateSyncVarSetter(td, fd, originalName, dirtyBit, netIdField, ref WeavingFailed);
  342. //NOTE: is property even needed? Could just use a setter function?
  343. //create the property
  344. PropertyDefinition propertyDefinition = new PropertyDefinition($"Network{originalName}", PropertyAttributes.None, fd.FieldType)
  345. {
  346. GetMethod = get,
  347. SetMethod = set
  348. };
  349. //add the methods and property to the type.
  350. td.Methods.Add(get);
  351. td.Methods.Add(set);
  352. td.Properties.Add(propertyDefinition);
  353. syncVarAccessLists.replacementSetterProperties[fd] = set;
  354. // replace getter field if GameObject/NetworkIdentity so it uses
  355. // netId instead
  356. // -> only for GameObjects, otherwise an int syncvar's getter would
  357. // end up in recursion.
  358. if (fd.FieldType.IsNetworkIdentityField())
  359. {
  360. syncVarAccessLists.replacementGetterProperties[fd] = get;
  361. }
  362. }
  363. public (List<FieldDefinition> syncVars, Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds) ProcessSyncVars(TypeDefinition td, ref bool WeavingFailed)
  364. {
  365. List<FieldDefinition> syncVars = new List<FieldDefinition>();
  366. Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds = new Dictionary<FieldDefinition, FieldDefinition>();
  367. // the mapping of dirtybits to sync-vars is implicit in the order of the fields here. this order is recorded in m_replacementProperties.
  368. // start assigning syncvars at the place the base class stopped, if any
  369. int dirtyBitCounter = syncVarAccessLists.GetSyncVarStart(td.BaseType.FullName);
  370. // find syncvars
  371. foreach (FieldDefinition fd in td.Fields)
  372. {
  373. if (fd.HasCustomAttribute<SyncVarAttribute>())
  374. {
  375. if ((fd.Attributes & FieldAttributes.Static) != 0)
  376. {
  377. Log.Error($"{fd.Name} cannot be static", fd);
  378. WeavingFailed = true;
  379. continue;
  380. }
  381. if (fd.FieldType.IsGenericParameter)
  382. {
  383. Log.Error($"{fd.Name} has generic type. Generic SyncVars are not supported", fd);
  384. WeavingFailed = true;
  385. continue;
  386. }
  387. if (fd.FieldType.IsArray)
  388. {
  389. Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd);
  390. WeavingFailed = true;
  391. continue;
  392. }
  393. if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType))
  394. {
  395. Log.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd);
  396. }
  397. else
  398. {
  399. syncVars.Add(fd);
  400. ProcessSyncVar(td, fd, syncVarNetIds, 1L << dirtyBitCounter, ref WeavingFailed);
  401. dirtyBitCounter += 1;
  402. if (dirtyBitCounter > SyncVarLimit)
  403. {
  404. Log.Error($"{td.Name} has > {SyncVarLimit} SyncVars. Consider refactoring your class into multiple components", td);
  405. WeavingFailed = true;
  406. continue;
  407. }
  408. }
  409. }
  410. }
  411. // add all the new SyncVar __netId fields
  412. foreach (FieldDefinition fd in syncVarNetIds.Values)
  413. {
  414. td.Fields.Add(fd);
  415. }
  416. syncVarAccessLists.SetNumSyncVars(td.FullName, syncVars.Count);
  417. return (syncVars, syncVarNetIds);
  418. }
  419. }
  420. }