Skip to content

Commit

Permalink
feat: new generic Read and Write methods for all types (#2301)
Browse files Browse the repository at this point in the history
Currently in mirror,  there is no way for Mirror itself to invoke
readers and writers for types it does not know about.

This causes us to have to generate code for lists, dictionaries and messages.

This PR makes it so that if there is a reader and writer anywhere in the
code for type X, then the writer can be invoked like this:

```cs
X x = ...;
writer.Write<X>(x);
```

and the reader can be invoked like this:
```cs
X x = reader.Read<X>();
```
  • Loading branch information
paulpach committed Oct 1, 2020
1 parent 16c919e commit 85252c3
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 13 deletions.
16 changes: 16 additions & 0 deletions Assets/Mirror/Editor/Weaver/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ public static MethodReference MakeHostInstanceGeneric(this MethodReference self,
return Weaver.CurrentAssembly.MainModule.ImportReference(reference);
}

/// <summary>
/// Given a field of a generic class such as Writer<T>.write,
/// and a generic instance such as ArraySegment`int
/// Creates a reference to the specialized method ArraySegment`int`.get_Count
/// <para> Note that calling ArraySegment`T.get_Count directly gives an invalid IL error </para>
/// </summary>
/// <param name="self"></param>
/// <param name="instanceType">Generic Instance eg Writer<int></param>
/// <returns></returns>
public static FieldReference SpecializeField(this FieldReference self, GenericInstanceType instanceType)
{
FieldReference reference = new FieldReference(self.Name, self.FieldType, instanceType);

return Weaver.CurrentAssembly.MainModule.ImportReference(reference);
}

public static CustomAttribute GetCustomAttribute<TAttribute>(this ICustomAttributeProvider method)
{
foreach (CustomAttribute ca in method.CustomAttributes)
Expand Down
71 changes: 58 additions & 13 deletions Assets/Mirror/Editor/Weaver/Processors/ReaderWriterProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// finds all readers and writers and register them
using System.IO;
using System;
using System.Linq;
using Mono.CecilX;
using Mono.CecilX.Cil;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;

namespace Mirror.Weaver
{
Expand All @@ -14,20 +18,12 @@ public static void Process(AssemblyDefinition CurrentAssembly)

foreach (Assembly unityAsm in CompilationPipeline.GetAssemblies())
{
if (unityAsm.name != CurrentAssembly.Name.Name)
if (unityAsm.name == "Mirror")
{
try
using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver())
using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(unityAsm.outputPath, new ReaderParameters { ReadWrite = false, ReadSymbols = false, AssemblyResolver = asmResolver }))
{
using (DefaultAssemblyResolver asmResolver = new DefaultAssemblyResolver())
using (AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(unityAsm.outputPath, new ReaderParameters { ReadWrite = false, ReadSymbols = false, AssemblyResolver = asmResolver }))
{
ProcessAssemblyClasses(CurrentAssembly, assembly);
}
}
catch (FileNotFoundException)
{
// During first import, this gets called before some assemblies
// are built, just skip them
ProcessAssemblyClasses(CurrentAssembly, assembly);
}
}
}
Expand Down Expand Up @@ -97,5 +93,54 @@ static void LoadDeclaredReaders(AssemblyDefinition currentAssembly, TypeDefiniti
Readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method));
}
}

private static bool IsEditorAssembly(AssemblyDefinition currentAssembly)
{
return currentAssembly.MainModule.AssemblyReferences.Any(assemblyReference =>
assemblyReference.Name == nameof(UnityEditor)
) ;
}

/// <summary>
/// Creates a method that will store all the readers and writers into
/// <see cref="Writer{T}.write"/> and <see cref="Reader{T}.read"/>
///
/// The method will be marked InitializeOnLoadMethodAttribute so it gets
/// executed before mirror runtime code
/// </summary>
/// <param name="currentAssembly"></param>
public static void InitializeReaderAndWriters(AssemblyDefinition currentAssembly)
{
var rwInitializer = new MethodDefinition("InitReadWriters", MethodAttributes.Public |
MethodAttributes.Static,
WeaverTypes.Import(typeof(void)));

System.Reflection.ConstructorInfo attributeconstructor = typeof(RuntimeInitializeOnLoadMethodAttribute).GetConstructor(new [] { typeof(RuntimeInitializeLoadType)});

CustomAttribute customAttributeRef = new CustomAttribute(currentAssembly.MainModule.ImportReference(attributeconstructor));
customAttributeRef.ConstructorArguments.Add(new CustomAttributeArgument(WeaverTypes.Import<RuntimeInitializeLoadType>(), RuntimeInitializeLoadType.BeforeSceneLoad));
rwInitializer.CustomAttributes.Add(customAttributeRef);

if (IsEditorAssembly(currentAssembly))
{
// editor assembly, add InitializeOnLoadMethod too. Useful for the editor tests
System.Reflection.ConstructorInfo initializeOnLoadConstructor = typeof(InitializeOnLoadMethodAttribute).GetConstructor(new Type[0]);
CustomAttribute initializeCustomConstructorRef = new CustomAttribute(currentAssembly.MainModule.ImportReference(initializeOnLoadConstructor));
rwInitializer.CustomAttributes.Add(initializeCustomConstructorRef);
}

ILProcessor worker = rwInitializer.Body.GetILProcessor();

Writers.InitializeWriters(worker);
Readers.InitializeReaders(worker);

worker.Append(worker.Create(OpCodes.Ret));

Weaver.WeaveLists.ConfirmGeneratedCodeClass();
TypeDefinition generateClass = Weaver.WeaveLists.generateContainerClass;

generateClass.Methods.Add(rwInitializer);
}

}
}
35 changes: 35 additions & 0 deletions Assets/Mirror/Editor/Weaver/Readers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -411,5 +411,40 @@ static void ReadAllFields(TypeReference variable, ILProcessor worker)
fields++;
}
}

/// <summary>
/// Save a delegate for each one of the readers into <see cref="Reader{T}.read"/>
/// </summary>
/// <param name="worker"></param>
internal static void InitializeReaders(ILProcessor worker)
{
ModuleDefinition module = Weaver.CurrentAssembly.MainModule;

TypeReference genericReaderClassRef = module.ImportReference(typeof(Reader<>));

System.Reflection.FieldInfo fieldInfo = typeof(Reader<>).GetField(nameof(Reader<object>.read));
FieldReference fieldRef = module.ImportReference(fieldInfo);
TypeReference networkReaderRef = module.ImportReference(typeof(NetworkReader));
TypeReference funcRef = module.ImportReference(typeof(Func<,>));
MethodReference funcConstructorRef = module.ImportReference(typeof(Func<,>).GetConstructors()[0]);

foreach (MethodReference readFunc in readFuncs.Values)
{
TypeReference dataType = readFunc.ReturnType;

// create a Func<NetworkReader, T> delegate
worker.Append(worker.Create(OpCodes.Ldnull));
worker.Append(worker.Create(OpCodes.Ldftn, readFunc));
GenericInstanceType funcGenericInstance = funcRef.MakeGenericInstanceType(networkReaderRef, dataType);
MethodReference funcConstructorInstance = funcConstructorRef.MakeHostInstanceGeneric(funcGenericInstance);
worker.Append(worker.Create(OpCodes.Newobj, funcConstructorInstance));

// save it in Writer<T>.write
GenericInstanceType genericInstance = genericReaderClassRef.MakeGenericInstanceType(dataType);
FieldReference specializedField = fieldRef.SpecializeField(genericInstance);
worker.Append(worker.Create(OpCodes.Stsfld, specializedField));
}

}
}
}
2 changes: 2 additions & 0 deletions Assets/Mirror/Editor/Weaver/Weaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ static bool Weave(string assName, IEnumerable<string> dependencies)

if (modified)
{
ReaderWriterProcessor.InitializeReaderAndWriters(CurrentAssembly);

// write to outputDir if specified, otherwise perform in-place write
WriterParameters writeParams = new WriterParameters { WriteSymbols = true };
CurrentAssembly.Write(writeParams);
Expand Down
37 changes: 37 additions & 0 deletions Assets/Mirror/Editor/Weaver/Writers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using Mono.CecilX;
using Mono.CecilX.Cil;
using Mono.CecilX.Rocks;

namespace Mirror.Weaver
{
Expand Down Expand Up @@ -433,5 +434,41 @@ private static void GenerateFor(ILProcessor worker, Action body)
worker.Append(worker.Create(OpCodes.Ldloc_0));
worker.Append(worker.Create(OpCodes.Blt, labelBody));
}

/// <summary>
/// Save a delegate for each one of the writers into <see cref="Writer{T}.write"/>
/// </summary>
/// <param name="worker"></param>
internal static void InitializeWriters(ILProcessor worker)
{
ModuleDefinition module = Weaver.CurrentAssembly.MainModule;

TypeReference genericWriterClassRef = module.ImportReference(typeof(Writer<>));

System.Reflection.FieldInfo fieldInfo = typeof(Writer<>).GetField(nameof(Writer<object>.write));
FieldReference fieldRef = module.ImportReference(fieldInfo);
TypeReference networkWriterRef = module.ImportReference(typeof(NetworkWriter));
TypeReference actionRef = module.ImportReference(typeof(Action<,>));
MethodReference actionConstructorRef = module.ImportReference(typeof(Action<,>).GetConstructors()[0]);

foreach (MethodReference writerMethod in writeFuncs.Values)
{

TypeReference dataType = writerMethod.Parameters[1].ParameterType;

// create a Action<NetworkWriter, T> delegate
worker.Append(worker.Create(OpCodes.Ldnull));
worker.Append(worker.Create(OpCodes.Ldftn, writerMethod));
GenericInstanceType actionGenericInstance = actionRef.MakeGenericInstanceType(networkWriterRef, dataType);
MethodReference actionRefInstance = actionConstructorRef.MakeHostInstanceGeneric(actionGenericInstance);
worker.Append(worker.Create(OpCodes.Newobj, actionRefInstance));

// save it in Writer<T>.write
GenericInstanceType genericInstance = genericWriterClassRef.MakeGenericInstanceType(dataType);
FieldReference specializedField = fieldRef.SpecializeField(genericInstance);
worker.Append(worker.Create(OpCodes.Stsfld, specializedField));
}
}

}
}
22 changes: 22 additions & 0 deletions Assets/Mirror/Runtime/NetworkReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@

namespace Mirror
{
/// <summary>
/// a class that holds readers for the different types
/// Note that c# creates a different static variable for each
/// type
/// This will be populated by the weaver
/// </summary>
/// <typeparam name="T"></typeparam>
public static class Reader<T>
{
public static Func<NetworkReader, T> read;
}

// Note: This class is intended to be extremely pedantic, and
// throw exceptions whenever stuff is going slightly wrong.
// The exceptions will be handled in NetworkServer/NetworkClient.
Expand Down Expand Up @@ -108,6 +120,16 @@ public override string ToString()
{
return "NetworkReader pos=" + Position + " len=" + Length + " buffer=" + BitConverter.ToString(buffer.Array, buffer.Offset, buffer.Count);
}

/// <summary>
/// Reads any data type that mirror supports
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Read<T>()
{
return Reader<T>.read(this);
}
}

// Mirror's Weaver automatically detects all NetworkReader function types,
Expand Down
22 changes: 22 additions & 0 deletions Assets/Mirror/Runtime/NetworkWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@

namespace Mirror
{
/// <summary>
/// a class that holds writers for the different types
/// Note that c# creates a different static variable for each
/// type
/// This will be populated by the weaver
/// </summary>
/// <typeparam name="T"></typeparam>
public static class Writer<T>
{
public static Action<NetworkWriter, T> write;
}

/// <summary>
/// Binary stream Writer. Supports simple types, buffers, arrays, structs, and nested types
/// <para>Use <see cref="NetworkWriterPool.GetWriter">NetworkWriter.GetWriter</see> to reduce memory allocation</para>
Expand Down Expand Up @@ -154,6 +166,16 @@ public void WriteUInt64(ulong value)
}

public void WriteInt64(long value) => WriteUInt64((ulong)value);

/// <summary>
/// Writes any type that mirror supports
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
public void Write<T>(T value)
{
Writer<T>.write(this, value);
}
}


Expand Down
31 changes: 31 additions & 0 deletions Assets/Mirror/Tests/Editor/NetworkReaderWriterTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using NUnit.Framework;

namespace Mirror.Tests
{
[TestFixture]
public class NetworkReaderWriterTest
{
[Test]
public void TestIntWriterNotNull()
{
Assert.That(Writer<int>.write, Is.Not.Null);
}

[Test]
public void TestIntReaderNotNull()
{
Assert.That(Reader<int>.read, Is.Not.Null);
}

[Test]
public void TestAccessingCustomWriterAndReader()
{
NetworkWriter writer = new NetworkWriter();
writer.Write<int>(3);
NetworkReader reader = new NetworkReader(writer.ToArray());
int copy = reader.Read<int>();

Assert.That(copy, Is.EqualTo(3));
}
}
}
11 changes: 11 additions & 0 deletions Assets/Mirror/Tests/Editor/NetworkReaderWriterTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 85252c3

Please sign in to comment.