Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Investigate support for Unity #644

Closed
jtattermusch opened this issue Jul 27, 2015 · 83 comments
Closed

Investigate support for Unity #644

jtattermusch opened this issue Jul 27, 2015 · 83 comments
Assignees

Comments

@jtattermusch
Copy link
Contributor

quoting @jskeet:
"There's suspicion the current version of C# protobufs won't work with Unity, but we can add another build target for that later when we find a need - we can create a new minor version of the NuGet package for that with no problems.

My experience with the "old" protobuf-csharp-port is that Unity on iOS has various issues, so I think we'll want to investigate them pretty thoroughly before trying to support it anyway - and we may need to conditionally compile out some features. We'll see... but I'd like to avoid having Unity support block a dotnetcore release."

also see #638

@dreis2211
Copy link

+1

2 similar comments
@Lyve1981
Copy link

+1

@sarky-de
Copy link

+1

@jskeet
Copy link
Contributor

jskeet commented Sep 25, 2015

Three +1s in the space of a few minutes suggests this is being linked to from somewhere - it would be nice if someone could provide the context. It would be even nicer if someone with Unity support could try this and inform us where there are problems :)

@sarky-de
Copy link

I can only speak for myself, but in my case we are developing a mobile game with Unity and a Java backend. In order to keep our client compatible with iOS, we are using protobuf-net. Unfortunately, protobuf-net has some known bugs and does not support the latest Protobuf versions, so an official solution from Google would be very interesting for us.

@jskeet
Copy link
Contributor

jskeet commented Sep 25, 2015

@jayallanjethwa: But how did you happen to reach this issue? It seems unlikely that three people would find an issue raised nearly two months ago entirely independently but within the same 5 minute window. I'm only asking so that I can try to get more context that might help resolve the issue. (I'd love to be able to support Unity - but I foresee some tricky issues, based on previous experience of bug reports.)

@asuuma
Copy link

asuuma commented Sep 25, 2015

@jskeet I started to use protobuf v3 in Unity, and I found two problems.
First of all, Unity is using .NET 3.5 and there were some compile errors when I copied to Unity project, so I had to modify some code with .NET 4.5 specific. This made me working protobuf v3 with Unity for simple case. Secondly, when I trying JsonFormatter.Format(IMessage) in iOS, Unity will threw exception of System.Reflection.Emit is not permitted., because iOS doesn't allow JIT.
So far I found these problems, but there may be other problems because I am not using full functions of v3.

@jskeet
Copy link
Contributor

jskeet commented Sep 25, 2015

@asuuma: Right. Supporting both .NET 3.5 and modern PCLs is painful. I'm hoping that it won't be too long before Unity moves on in terms of runtime - after all, .NET 4.0 was released a long time ago. Could you give the details of the stack trace around Reflection.Emit? I suspect the problem is the use of expression trees, which would be feasible to fix, though annoying.

(I'm going to need to balance the importance of supporting a wide variety of platforms with the additional code maintenance headaches and possible performance degredations of doing so. The more I know before making this judgement call, the better...)

@asuuma
Copy link

asuuma commented Sep 28, 2015

@jskeet Yeah, everyone are hoping to move on newer runtime in near future... They opened roadmap of Unity and it says they are researching about upgrading .NET profile.
Here is the stack trace.

NotSupportedException: /Users/builduser/buildslave/unity/build/Tools/il2cpp/il2cpp/libil2cpp/icalls/mscorlib/System.Reflection.Emit/DynamicMethod.cpp(21) : Unsupported internal call for IL2CPP:DynamicMethod::create_dynamic_method - System.Reflection.Emit is not supported.
  at System.Reflection.Emit.DynamicMethod.CreateDelegate (System.Type delegateType, System.Object target) [0x00000] in <filename unknown>:0 
  at System.Linq.Expressions.Expression`1[TDelegate].Compile () [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FieldAccessorBase..ctor (System.Reflection.PropertyInfo property, Google.Protobuf.Reflection.FieldDescriptor descriptor) [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.SingleFieldAccessor..ctor (System.Reflection.PropertyInfo property, Google.Protobuf.Reflection.FieldDescriptor descriptor) [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FieldDescriptor.CreateAccessor (System.String propertyName) [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FieldDescriptor.CrossLink () [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.MessageDescriptor.CrossLink () [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FileDescriptor.CrossLink () [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FileDescriptor.BuildFrom (Google.Protobuf.ByteString descriptorData, Google.Protobuf.Reflection.FileDescriptorProto proto, Google.Protobuf.Reflection.FileDescriptor[] dependencies, Boolean allowUnknownDependencies, Google.Protobuf.Reflection.GeneratedCodeInfo generatedCodeInfo) [0x00000] in <filename unknown>:0 
  at Google.Protobuf.Reflection.FileDescriptor.InternalBuildGeneratedFileFrom (System.Byte[] descriptorData, Google.Protobuf.Reflection.FileDescriptor[] dependencies, Google.Protobuf.Reflection.GeneratedCodeInfo generatedCodeInfo) [0x00000] in <filename unknown>:0 
  at Test..cctor () [0x00000] in <filename unknown>:0 
  at Test.get_Descriptor () [0x00000] in <filename unknown>:0 
  at Google.Protobuf.JsonFormatter.Format (IMessage message) [0x00000] in <filename unknown>:0 

@jskeet
Copy link
Contributor

jskeet commented Sep 28, 2015

Right, thanks - so that is expression tree compilation. It's definitely feasible to write a workaround for that particular one.

@dreis2211
Copy link

@asuuma Could you elaborate on what you did to workaround the .NET version mismatch?

@asuuma
Copy link

asuuma commented Oct 23, 2015

@dreis2211 Oh very sorry, I missed your message.
Here is diff of what I changed, just fixing compile errors.

diff -u -r Google.Protobuf/Compatibility/PropertyInfoExtensions.cs protubuf-unity/Compatibility/PropertyInfoExtensions.cs
--- Google.Protobuf/Compatibility/PropertyInfoExtensions.cs 2015-09-10 20:05:51.000000000 +0900
+++ protubuf-unity/Compatibility/PropertyInfoExtensions.cs  2015-10-06 12:36:43.000000000 +0900
@@ -47,7 +47,7 @@
         /// </summary>
         internal static MethodInfo GetGetMethod(this PropertyInfo target)
         {
-            var method = target.GetMethod;
+            var method = target.GetGetMethod();
             return method != null && method.IsPublic ? method : null;
         }

@@ -57,7 +57,7 @@
         /// </summary>
         internal static MethodInfo GetSetMethod(this PropertyInfo target)
         {
-            var method = target.SetMethod;
+            var method = target.GetSetMethod();
             return method != null && method.IsPublic ? method : null;
         }
     }
diff -u -r Google.Protobuf/Compatibility/TypeExtensions.cs protubuf-unity/Compatibility/TypeExtensions.cs
--- Google.Protobuf/Compatibility/TypeExtensions.cs 2015-09-10 20:05:51.000000000 +0900
+++ protubuf-unity/Compatibility/TypeExtensions.cs  2015-10-06 12:36:43.000000000 +0900
@@ -51,7 +51,8 @@
         /// </summary>
         internal static bool IsValueType(this Type target)
         {
-            return target.GetTypeInfo().IsValueType;
+//            return target.GetTypeInfo().IsValueType;
+           return target.GetType().IsValueType;
         }

         /// <summary>
@@ -59,7 +60,8 @@
         /// </summary>
         internal static bool IsAssignableFrom(this Type target, Type c)
         {
-            return target.GetTypeInfo().IsAssignableFrom(c.GetTypeInfo());
+//            return target.GetTypeInfo().IsAssignableFrom(c.GetTypeInfo());
+           return target.GetType().IsAssignableFrom(c.GetType());
         }

         /// <summary>
@@ -72,9 +74,9 @@
             // GetDeclaredProperty only returns properties declared in the given type, so we need to recurse.
             while (target != null)
             {
-                var typeInfo = target.GetTypeInfo();
-                var ret = typeInfo.GetDeclaredProperty(name);
-                if (ret != null && ((ret.CanRead && ret.GetMethod.IsPublic) || (ret.CanWrite && ret.SetMethod.IsPublic)))
+                var typeInfo = target.GetType();
+                var ret = typeInfo.GetProperty(name);
+                if (ret != null && ((ret.CanRead && ret.GetGetMethod().IsPublic) || (ret.CanWrite && ret.GetSetMethod().IsPublic)))
                 {
                     return ret;
                 }
@@ -99,8 +101,8 @@
             // GetDeclaredMethod only returns methods declared in the given type, so we need to recurse.
             while (target != null)
             {
-                var typeInfo = target.GetTypeInfo();
-                var ret = typeInfo.GetDeclaredMethod(name);
+                var typeInfo = target.GetType();
+                var ret = typeInfo.GetMethod(name);
                 if (ret != null && ret.IsPublic)
                 {
                     return ret;
diff -u -r Google.Protobuf/JsonFormatter.cs protubuf-unity/JsonFormatter.cs
--- Google.Protobuf/JsonFormatter.cs    2015-09-10 20:05:51.000000000 +0900
+++ protubuf-unity/JsonFormatter.cs 2015-10-23 10:14:41.000000000 +0900
@@ -496,7 +496,7 @@
         private void WriteFieldMask(StringBuilder builder, IMessage value)
         {
             IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value);
-            AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase)));
+            AppendEscapedString(builder, string.Join(",", paths.Cast<string>().Select(s => ToCamelCase(s)).ToArray()));
         }

         /// <summary>
diff -u -r Google.Protobuf/WellKnownTypes/TimeExtensions.cs protubuf-unity/WellKnownTypes/TimeExtensions.cs
--- Google.Protobuf/WellKnownTypes/TimeExtensions.cs    2015-09-10 20:05:51.000000000 +0900
+++ protubuf-unity/WellKnownTypes/TimeExtensions.cs 2015-10-23 10:13:48.000000000 +0900
@@ -34,7 +34,6 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
-using System.Threading.Tasks;

 namespace Google.Protobuf.WellKnownTypes
 {

@zeroZshadow
Copy link

I've applied this patch (without the commented out code) and can confirm this work in unity3d 5.2.2f
This also keeps working when used in a WebGL export. Although i am not using any fancy protobuf features (only string, int, etc and repeated)

@jskeet
Copy link
Contributor

jskeet commented Nov 2, 2015

Note that with this patch, it won't work with CoreCLR, which is a more important initial target for us. I don't think it's feasible to support both platforms with a single binary.

@zeroZshadow
Copy link

Agreed, but it would be nice if unity could be supported somehow untill they finally decide to move away from their ancient mono version.
Even a simple readme note for unity users revering to this issue ticket would be enough.

@jskeet
Copy link
Contributor

jskeet commented Nov 2, 2015

I wouldn't feel comfortable claiming something was "supported" via just a patch with consumers building the code themselves afterwards. This patch is a good starting point for fuller support, but I'd rather get the first version publicly released with just CoreCLR (and therefore normal desktop .NET) support to start with, then revisit.

@gyf19
Copy link

gyf19 commented Nov 6, 2015

+1

@djungelorm
Copy link

+1

I've also applied the patch, and have protobuf v3.0.0-beta-2 working in my mod for Kerbal Space Program (running in Unity). I also ran the nunit tests which all passed (although I removed Google.Protobuf.Test.Compatibility)

@dreis2211
Copy link

@jskeet Thanks for your statement. Do you have a rough idea of how the timeline for this could look like?

@jskeet
Copy link
Contributor

jskeet commented Jan 24, 2016

I don't have any plans at the moment... I'd be very surprised to see it before protobuf 3.0 goes GA. We can look at it again after that.

It's not just a matter of fixing the code itself - it's making sure we can test and maintain it appropriately.

(Any sign of Unity moving off the ancient CLR they use yet? I'd hope that they're looking into CoreCLR now it's nearing release...)

@FrankNine
Copy link

I think it is unlikely that Unity moves off their CLR. They have their own Mono fork and it contains a bug which is already fixed in original Mono repo 5 years ago. I guess merging branches forked at least 5 years ago would be a heavy toll. Plus they have to make their il2cpp work after the update.

But still, I wish that would happen. I'd love to use protobuf and google.apis.v3 in Unity.

@zeroZshadow
Copy link

You are able to use Protobuf just fine in unity3d though? I'm currently using it for serializing my network packages.

@jskeet
Copy link
Contributor

jskeet commented Feb 3, 2016

@zeroZshadow: My guess is that you're using protobuf-csharp-port, the old proto2 implementation... which apparently worked on Unity if you were lucky. We repeatedly ran into issues to do with JIT or AOT compilation, suspecting them to be bugs with the very old version of Mono being used.

@zeroZshadow
Copy link

@jskeet I'm using whatever is here https://github.com/google/protobuf/tree/master/csharp/src/Google.Protobuf together with proto3

@jskeet
Copy link
Contributor

jskeet commented Feb 3, 2016

@zeroZshadow: So how are you using that? I wouldn't have expected you to be able to install the NuGet package, for example - it targets CLRv4. Which version of Unity3D are you using?

@zeroZshadow
Copy link

@jskeet I've copied that entire folder to Assets/Plugins, then applied the fix that is posted by asuuma.
I'm using Unity 5.3.2, but earlyer versions also had no issues.

@jskeet
Copy link
Contributor

jskeet commented Feb 3, 2016

Okay - that makes a bit more sense. You should be aware that this isn't an officially-supported scenario at the moment. I make no guarantees about it - you may well find it works in simply situations, but that some more advanced features break :( Obviously we'd like to support Unity eventually - but the nature of the platform does make it hard to support properly, and we want to get the support for more modern platforms rock-solid first.

@zaneclaes
Copy link

zaneclaes commented Mar 24, 2018

@jtattermusch this is relevant to our ongoing work to get GRPC working in Unity. Once I started introducing more complicated messages to my project (instead of the simple Healthcheck I was running), I ran into this System.Reflection.Emit issue. It looks like my particular errors are related to the google.protobuf.Timestamp well-known type, so I'm going to first try replacing its usage in my project.

I'd also be interested @jskeet 's fork, but need to research how to cross-compile the necessary DLL on OSX, as the default installation instructions obviously yield dylibs.

Given that this issue has been open for almost 3 years now... any chance we can drive this home?


edit After replacing all instances of google.protobuf.Timestamp in my protobuf definitions with a simple double (to encode unix epoch time instead), everything worked using the current NuGet Protobuf package :)

@chenditc
Copy link

I was able to get grpc up and running on iOS 11 + Unity 2017 + .NET4.6, I documented the process in my blog. Hope this could help for you guys.

I was thinking creating a branch for Unity support, but I was only able to get it up and running on iOS and specifically .NET 4.6, so maybe there should be a better way to support this.

@zaneclaes
Copy link

zaneclaes commented Mar 24, 2018 via email

@zaneclaes
Copy link

zaneclaes commented Mar 24, 2018 via email

@chenditc
Copy link

Sounds Great! Awesome work! I'm looking forward to see grpc officially support Unity!

@jtattermusch
Copy link
Contributor Author

@zaneclaes can you link to the System.Reflection.Emit issue?

Ad the question how to proceed with improving support:

  • the best way to help with stuff that has been stuck for long time is to come up with a series of small PRs that incrementally fix the issue. Contributions are welcome.

@zaneclaes
Copy link

zaneclaes commented Mar 26, 2018

@jtattermusch er, this is the System.Reflection.Emit issue -- see the stacktraces above. My understanding is that @jskeet 's open PR fixes the issue by avoiding the use of expression trees, but I have been unable to validate this (if someone can provide a compiled DLL of his work as a drop-in replacement for the NuGet package, I'd be happy to do so): #3794

Here's my own stacktrace to add to the mix. Note that I still have this issue if I attempt to stringify any of my messages on the iOS client. For example, the following stacktrace is triggered by $"Message {msg}" where msg is an instance of a BootstrapRes (pb::IMessage):

NotSupportedException: /Users/builduser/buildslave/unity/build/External/il2cpp/il2cpp/libil2cpp/icalls/mscorlib/System.Reflection.Emit/AssemblyBuilder.cpp(20) : Unsupported internal call for IL2CPP:AssemblyBuilder::basic_init - System.Reflection.Emit is not supported.
  at System.Reflection.Emit.AssemblyBuilder..ctor (System.Reflection.AssemblyName n, System.String directory, System.Reflection.Emit.AssemblyBuilderAccess access, System.Boolean corlib_internal) [0x00000] in <00000000000000000000000000000000>:0 
  at System.AppDomain.DefineDynamicAssembly (System.Reflection.AssemblyName name, System.Reflection.Emit.AssemblyBuilderAccess access, System.String dir, System.Security.Policy.Evidence evidence, System.Security.PermissionSet requiredPermissions, System.Security.PermissionSet optionalPermissions, System.Security.PermissionSet refusedPermissions, System.Boolean isSynchronized) [0x00000] in <00000000000000000000000000000000>:0 
  at System.AppDomain.DefineDynamicAssembly (System.Reflection.AssemblyName name, System.Reflection.Emit.AssemblyBuilderAccess access) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.Emit.DynamicMethod+AnonHostModuleHolder..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.Emit.DynamicMethod..ctor (System.String name, System.Reflection.MethodAttributes attributes, System.Reflection.CallingConventions callingConvention, System.Type returnType, System.Type[] parameterTypes, System.Type owner, System.Reflection.Module m, System.Boolean skipVisibility, System.Boolean anonHosted) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.Emit.DynamicMethod..ctor (System.String name, System.Type returnType, System.Type[] parameterTypes, System.Boolean restrictedSkipVisibility) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Linq.Expressions.Compiler.LambdaCompiler..ctor (System.Linq.Expressions.Compiler.AnalyzedTree tree, System.Linq.Expressions.LambdaExpression lambda) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Linq.Expressions.Compiler.LambdaCompiler.Compile (System.Linq.Expressions.LambdaExpression lambda) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Linq.Expressions.Expression`1[TDelegate].Compile (System.Boolean preferInterpretation) [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FieldAccessorBase..ctor (System.Reflection.PropertyInfo property, Google.Protobuf.Reflection.FieldDescriptor descriptor) [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.SingleFieldAccessor..ctor (System.Reflection.PropertyInfo property, Google.Protobuf.Reflection.FieldDescriptor descriptor) [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FieldDescriptor.CreateAccessor () [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FieldDescriptor.CrossLink () [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.MessageDescriptor.CrossLink () [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FileDescriptor.CrossLink () [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FileDescriptor.BuildFrom (Google.Protobuf.ByteString descriptorData, Google.Protobuf.Reflection.FileDescriptorProto proto, Google.Protobuf.Reflection.FileDescriptor[] dependencies, System.Boolean allowUnknownDependencies, Google.Protobuf.Reflection.GeneratedClrTypeInfo generatedCodeInfo) [0x00000] in <00000000000000000000000000000000>:0 
  at Google.Protobuf.Reflection.FileDescriptor.FromGeneratedCode (System.Byte[] descriptorData, Google.Protobuf.Reflection.FileDescriptor[] dependencies, Google.Protobuf.Reflection.GeneratedClrTypeInfo generatedCodeInfo) [0x00000] in <00000000000000000000000000000000>:0 
  at Moongate.GatewayReflection..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Moongate.BootstrapRes.get_Descriptor () [0x00000] in <00000000000000000000000000000000>:0 

@jtattermusch
Copy link
Contributor Author

@zaneclaes do you know about this one #3794?

@zaneclaes
Copy link

Yep; I referenced it in my last two comments @jtattermusch ; again:

My understanding is that @jskeet 's open PR fixes the issue by avoiding the use of expression trees, but I have been unable to validate this (if someone can provide a compiled DLL of his work as a drop-in replacement for the NuGet package, I'd be happy to do so)

@zaneclaes
Copy link

Since I have a work-around by avoiding the use of the methods which trigger the error, I haven't invested in cross-compiling the PR just yet because I'm on OSX ;)

@jtattermusch
Copy link
Contributor Author

@zaneclaes building the Google.Protobuf assembly should be trivial:

  • install dotnet SDK
  • dotnet restore Google.Protobuf.sln
  • dotnet build Google.Protobuf.sln

@jtattermusch
Copy link
Contributor Author

#3794 has been merged.

@zaneclaes
Copy link

Excellent, thanks @jtattermusch and @jskeet ! Sorry I didn't get to test this before it got merged; this isn't my main work and I have limited time each day ;) Look forward to NuGet being updated!

@haberman
Copy link
Member

haberman commented Jun 8, 2018

Closing as this appears to be fixed.

@haberman haberman closed this as completed Jun 8, 2018
@zaneclaes
Copy link

@haberman I don't think this is really "fixed." There is limited experimental support for Android alone. It still doesn't work on many platforms, and even those it does work on require a lot of voodoo and undocumented tweaking to use.

@xfxyjwf xfxyjwf reopened this Jun 11, 2018
@xfxyjwf
Copy link
Contributor

xfxyjwf commented Jun 11, 2018

@zaneclaes Can you help give a summary on what's working and what's not working?

@zaneclaes
Copy link

I can try. The core challenge is that grpc_csharp_ext needs to compiled natively for each platform, and placed into the Assets/Plugins directory of Unity.

  • If you use the NuGet package, you can dig into the compiled binaries and copy grpc_csharp_ext.dll and libgrpc_csharp_ext.so into this directory to make Windows and Linux work.
  • To make OSX work, you need to rename libgrpc_csharp_ext.dylib to a bundle before copying.
  • To make Android work, you need to use the special compilation implemented in this thread and copy its libgrpc_csharp_ext.so
  • There is a partially working iOS solution I've detailed on my blog (it doesn't support bitcode yet).
  • There is no working WebAssembly solution yet.
  • Other platforms may also be broken (I've only enumerated the "big ones.")

I may have also made some additional small tweaks to GRPC core, which I'll have to dig a little deeper on. But that covers the main points.

FWIW, Unity has its own distribution channel (the asset store), which is probably the better route for distributing a working GRPC build to Unity users, which I'd be very interested in building/maintaining, if I could get some strong support from the rest of the community.

@jskeet
Copy link
Contributor

jskeet commented Jun 11, 2018

Those sound like gRPC issues - this issue is about just protobuf, which I believe is in a rather better state for Unity now, although it still requires some code to get reflection working correctly.

@zaneclaes
Copy link

Oh, gosh. So sorry; I spent much more time interacting with the gRPC side of this and totally overlooked that this issue was in Protobufs. It seems that this can be closed! Apologies.

@xfxyjwf
Copy link
Contributor

xfxyjwf commented Jun 11, 2018

Thanks for the updates!

@xfxyjwf xfxyjwf closed this as completed Jun 11, 2018
chaosky pushed a commit to chaosky/protobuf-net that referenced this issue Sep 10, 2020
## protobuf-net 分析

[protobuf-net](https://github.com/protobuf-net/protobuf-net) 当前的 GC 问题有:

- 序列化
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`property.GetValue(value, null)`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`Tail.Write(value, dest)`
  - foreach。`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`
- 反序列化GC
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`property.SetValue`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`Tail.Read(oldVal, source)`
  - 列表创建。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`value = Activator.CreateInstance(concreteType)`
  - 列表扩容。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`list.Add`
  - byte[]创建。函数`ProtoBuf.ProtoReader.AppendBytes`中的字节数组创建
  - Pb对象创建。函数`ProtoBuf.Serializers.TypeSerializer.Read`中的`CreateInstance(source)`

## protobuf-net-gc-optimization 针对 protobuf-net 进行的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization)

- 去反射:对指定的协议进行Hook。
  - 反射产生的地方在protobuf-net的装饰类中,具体是PropertyDecorator,没有去写工具自动生成Wrap文件,而是对指定的协议进行Hook。`CustomDecorator`, `ICustomProtoSerializer`及其实现
- foreach
  - foreach 对列表来说改写遍历方式,没有对它进行优化,因为 Unity 5.5 以后版本这个问题就不存在了
- 无GC装箱
  - 消除装箱操作。重构代码,而 protobuf-net 内部大量使用了object 进行参数传递,这使得用泛型编程来消除 GC 变得不太现实。实现了一个无 GC 版本的装箱拆箱类`ValueObject`
- 使用对象池
  - 对于 protobuf-net反序列化的时候会创建pb对象这一点,最合理的方式是使用对象池,Hook 住protobuf-net 创建对象的地方,从对象池中取对象,而不是新建对象,用完以后再执行回收。`IProtoPool`及实现
- 使用字节缓存池
  - 对于 new byte[] 操作的 GC 优化也是一样的,只不过这里使用的缓存池是针对字节数组而非 pb 对象,实现了一套通用的字节流与字节 buffer 缓存池`StreamBufferPool`,每次需要字节buffer时从中取,用完以后放回。

### protobuf-net-gc-optimization 的其他优化

- BetterDelegate:泛型委托包装类,针对深层函数调用树中使用泛型委托作为函数参数进行传递时代码编写困难的问题。
- BetterLinkedList:无GC链表
- BetterStringBuilder:无GC版StrigBuilder
- StreamBufferPool:字节流与字节buffer缓存池
- ValueObject:无GC装箱拆箱
- ObjPool:通用对象池

关键节点:

- LinkedList当自定义结构做链表节点,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、Find、FindLast每次都有GC产生
- 所有委托必须缓存,产生GC的测试一律是因为每次调用都生成了一个新的委托
- List<T>对于自定义结构做列表项,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、IndexOf、sort每次都有GC产生;对于Sort,需要传递一个委托。这两点的实践上面都已经说明。

## 针对 protobuf-net-gc-optimization 的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization) 使用的`protobuf-net`是 2015 年之前的版本,当前项目使用的是 protobuf-net 2.4.5, 把 protobuf-net-gc-optimization 相关的优化合并到了 protobuf-net 2.x 版本上。

### foreach

`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`

因为 C# 不支持泛型协变,上述 foreach 循环还会产生GC,需要针对性的优化。

优化前:

```csharp
foreach (object subItem in (IEnumerable)value)
{
    if (checkForNull && subItem == null) { throw new NullReferenceException(); }
    Tail.Write(ValueObject.TryGet(subItem), dest);
}
```

优化后:

```csharp
if (value is IList list)
{
    for (int i = 0; i < list.Count; i++)
    {
        var subItem = list[i];
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
else
{
    foreach (object subItem in (IEnumerable)value)
    {
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
```

### 其他优化

`protobuf-net`的`BufferPool`在(2018.6.8)[https://github.com/protobuf-net/protobuf-net/commit/9718b9221ee0c2aa13509d0a258a0728d3fc3210#diff-3df8aa4e7ab0d7118f25612197fbe78d]修改为`弱引用(WeakReference)`实现内部缓存,之前的[老版本]为(https://github.com/protobuf-net/protobuf-net/commit/15fa224b3ceab2cdf99012d999307b3435936665#diff-3df8aa4e7ab0d7118f25612197fbe78d)。弱引用会导致 Unity Profile 时,每次调用缓存失效,创建新的对象(56B)。这里使用老的版本。

## 需要再次确认的代码

- `ProtoBuf.BufferPool`内部有锁,用于 ProtoWriter,ProtoReader 读写, 能否优化掉
- `ProtoBuf.ProtoReader.AppendBytes`: `protobuf-net-gc-optimization`的注释(// TODO:这里还有漏洞,但是我们目前的项目不会走到这)需要再次确认

## 测试结果

`Assets/TestScenes/TestProtoBuf/TestProtoBuf.unity` 及 `TestProtoBuf.Test5` 测试结果, 开启 deep profile:

|             | 序列化 GC/time | 反序列化GC GC/time |
| ----------- | -------------- | ------------------ |
| 优化前₁     | 0.8k/0.21ms    | 1.3k/0.23ms        |
| 优化后₂     | 80B+56B/0.23ms | 0B+56B/0.20ms      |
| 再次优化后₃ | 0B             | 0B                 |

1. https://github.com/protobuf-net/protobuf-net 代码
2. https://github.com/smilehao/protobuf-net-gc-optimization 修改合并到 protobuf-net 2.4.5 后。两次List枚举器的获取,每次40B
3. 2.4.5-gc-optimization 代码

## google protobuf 分析

[protocolbuffers/protobuf 3.13.0](https://github.com/protocolbuffers/protobuf/tree/v3.13.0)需要 .NET Standard 2.1,Unity 只支持 .NET Standard 2.0,需要添加额外的DLL[1](protocolbuffers/protobuf#7668), [2](protocolbuffers/protobuf#7252

## 参考资料

- [九:Unity 帧同步补遗(性能优化)](https://zhuanlan.zhihu.com/p/39478710)
- [Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践](https://www.cnblogs.com/SChivas/p/7898166.html)
- [Google protobuf 重用缓存方法](protocolbuffers/protobuf#644)
- [unity 官方 GC 优化教程 - Fixing Performance Problems - 2019.3](https://learn.unity.com/tutorial/fixing-performance-problems-2019-3?uv=2019.3#)
chaosky pushed a commit to chaosky/protobuf-net that referenced this issue Sep 10, 2020
## protobuf-net 分析

[protobuf-net](https://github.com/protobuf-net/protobuf-net) 当前的 GC 问题有:

- 序列化
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`property.GetValue(value, null)`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Write`中的`Tail.Write(value, dest)`
  - foreach。`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`
- 反序列化GC
  - 反射。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`property.SetValue`
  - 装箱拆箱。函数`ProtoBuf.Serializers.PropertyDecorator.Read`中的`Tail.Read(oldVal, source)`
  - 列表创建。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`value = Activator.CreateInstance(concreteType)`
  - 列表扩容。函数`ProtoBuf.Serializers.ListDecorator.Read`中的`list.Add`
  - byte[]创建。函数`ProtoBuf.ProtoReader.AppendBytes`中的字节数组创建
  - Pb对象创建。函数`ProtoBuf.Serializers.TypeSerializer.Read`中的`CreateInstance(source)`

## protobuf-net-gc-optimization 针对 protobuf-net 进行的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization)

- 去反射:对指定的协议进行Hook。
  - 反射产生的地方在protobuf-net的装饰类中,具体是PropertyDecorator,没有去写工具自动生成Wrap文件,而是对指定的协议进行Hook。`CustomDecorator`, `ICustomProtoSerializer`及其实现
- foreach
  - foreach 对列表来说改写遍历方式,没有对它进行优化,因为 Unity 5.5 以后版本这个问题就不存在了
- 无GC装箱
  - 消除装箱操作。重构代码,而 protobuf-net 内部大量使用了object 进行参数传递,这使得用泛型编程来消除 GC 变得不太现实。实现了一个无 GC 版本的装箱拆箱类`ValueObject`
- 使用对象池
  - 对于 protobuf-net反序列化的时候会创建pb对象这一点,最合理的方式是使用对象池,Hook 住protobuf-net 创建对象的地方,从对象池中取对象,而不是新建对象,用完以后再执行回收。`IProtoPool`及实现
- 使用字节缓存池
  - 对于 new byte[] 操作的 GC 优化也是一样的,只不过这里使用的缓存池是针对字节数组而非 pb 对象,实现了一套通用的字节流与字节 buffer 缓存池`StreamBufferPool`,每次需要字节buffer时从中取,用完以后放回。

### protobuf-net-gc-optimization 的其他优化

- BetterDelegate:泛型委托包装类,针对深层函数调用树中使用泛型委托作为函数参数进行传递时代码编写困难的问题。
- BetterLinkedList:无GC链表
- BetterStringBuilder:无GC版StrigBuilder
- StreamBufferPool:字节流与字节buffer缓存池
- ValueObject:无GC装箱拆箱
- ObjPool:通用对象池

关键节点:

- LinkedList当自定义结构做链表节点,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、Find、FindLast每次都有GC产生
- 所有委托必须缓存,产生GC的测试一律是因为每次调用都生成了一个新的委托
- List<T>对于自定义结构做列表项,必须实现IEquatable<T>、IComparable<T>接口,否则Roemove、Cotains、IndexOf、sort每次都有GC产生;对于Sort,需要传递一个委托。这两点的实践上面都已经说明。

## 针对 protobuf-net-gc-optimization 的优化

[protobuf-net-gc-optimization](https://github.com/smilehao/protobuf-net-gc-optimization) 使用的`protobuf-net`是 2015 年之前的版本,当前项目使用的是 protobuf-net 2.4.5, 把 protobuf-net-gc-optimization 相关的优化合并到了 protobuf-net 2.x 版本上。

### foreach

`ProtoBuf.Serializers.ListDecorator.Write`中的`foreach (object subItem in (IEnumerable)value)`

因为 C# 不支持泛型协变,上述 foreach 循环还会产生GC,需要针对性的优化。

优化前:

```csharp
foreach (object subItem in (IEnumerable)value)
{
    if (checkForNull && subItem == null) { throw new NullReferenceException(); }
    Tail.Write(ValueObject.TryGet(subItem), dest);
}
```

优化后:

```csharp
if (value is IList list)
{
    for (int i = 0; i < list.Count; i++)
    {
        var subItem = list[i];
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
else
{
    foreach (object subItem in (IEnumerable)value)
    {
        if (checkForNull && subItem == null) { throw new NullReferenceException(); }
        Tail.Write(ValueObject.TryGet(subItem), dest);
    }
}
```

### 其他优化

`protobuf-net`的`BufferPool`在(2018.6.8)[https://github.com/protobuf-net/protobuf-net/commit/9718b9221ee0c2aa13509d0a258a0728d3fc3210#diff-3df8aa4e7ab0d7118f25612197fbe78d]修改为`弱引用(WeakReference)`实现内部缓存,之前的[老版本]为(https://github.com/protobuf-net/protobuf-net/commit/15fa224b3ceab2cdf99012d999307b3435936665#diff-3df8aa4e7ab0d7118f25612197fbe78d)。弱引用会导致 Unity Profile 时,每次调用缓存失效,创建新的对象(56B)。这里使用老的版本。

## 需要再次确认的代码

- `ProtoBuf.BufferPool`内部有锁,用于 ProtoWriter,ProtoReader 读写, 能否优化掉
- `ProtoBuf.ProtoReader.AppendBytes`: `protobuf-net-gc-optimization`的注释(// TODO:这里还有漏洞,但是我们目前的项目不会走到这)需要再次确认

## 测试结果

`Assets/TestScenes/TestProtoBuf/TestProtoBuf.unity` 及 `TestProtoBuf.Test5` 测试结果, 开启 deep profile:

|             | 序列化 GC/time | 反序列化GC GC/time |
| ----------- | -------------- | ------------------ |
| 优化前₁     | 0.8k/0.21ms    | 1.3k/0.23ms        |
| 优化后₂     | 80B+56B/0.23ms | 0B+56B/0.20ms      |
| 再次优化后₃ | 0B             | 0B                 |

1. https://github.com/protobuf-net/protobuf-net 代码
2. https://github.com/smilehao/protobuf-net-gc-optimization 修改合并到 protobuf-net 2.4.5 后。两次List枚举器的获取,每次40B
3. 2.4.5-gc-optimization 代码

## google protobuf 分析

[protocolbuffers/protobuf 3.13.0](https://github.com/protocolbuffers/protobuf/tree/v3.13.0)需要 .NET Standard 2.1,Unity 只支持 .NET Standard 2.0,需要添加额外的DLL[1](protocolbuffers/protobuf#7668), [2](protocolbuffers/protobuf#7252

## 参考资料

- [九:Unity 帧同步补遗(性能优化)](https://zhuanlan.zhihu.com/p/39478710)
- [Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践](https://www.cnblogs.com/SChivas/p/7898166.html)
- [Google protobuf 重用缓存方法](protocolbuffers/protobuf#644)
- [unity 官方 GC 优化教程 - Fixing Performance Problems - 2019.3](https://learn.unity.com/tutorial/fixing-performance-problems-2019-3?uv=2019.3#)
adellahlou pushed a commit to adellahlou/protobuf that referenced this issue Apr 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests