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

Change C# reflection to avoid using expression trees #3794

Merged
merged 5 commits into from
Mar 27, 2018

Conversation

jskeet
Copy link
Contributor

@jskeet jskeet commented Oct 24, 2017

This should work on Unity, Mono and .NET 3.5 as far as I'm aware.
It won't work on platforms where reflection itself is prohibited,
but that's a non-starter basically.

Creating a PR to start with, but I'd like to test it in other environments before merging.
Once this is in, we can look at potentially officially supporting .NET 3.5 and Unity (#644).

@anandolee
Copy link
Contributor

anandolee commented Oct 24, 2017

The original code does not work on platforms where reflection itself is prohibited ?

@jskeet
Copy link
Contributor Author

jskeet commented Oct 24, 2017

Indeed - but the hope is that this will work on more platforms. In Unity (at least on some platforms), I believe reflection works, but expression trees don't.

@jskeet
Copy link
Contributor Author

jskeet commented Nov 16, 2017

Humbug - doesn't quite work on Unity yet. At least one version of Mono used by Unity doesn't view enums and ints as convertible in the way that .NET does. Looking further...

@jskeet
Copy link
Contributor Author

jskeet commented Nov 16, 2017

I've now managed to get this code formatting JSON correctly on a sample Unity3d project, so I think this PR is ready for review and merging.

@jtattermusch
Copy link
Contributor

@jskeet there's been some progress towards supporting Unity in the grpc/grpc recently and some contributions have pointed out that I'd be good to have this PR merged.

Just to doublecheck - are you currently aware of any limitations why this PR shouldn't be merged?
(it looks like that there shouldn't be any noticeable performance impact as the ReflectionUtil is only used once when the message type are initialized?).

@jskeet
Copy link
Contributor Author

jskeet commented Mar 27, 2018

I'm not aware of any problems, and I'd welcome further review.

If anything, there should be a performance improvement as it doesn't require expression tree compilation... but this only affects reflection use cases (including JSON formatting and parsing).

Ideally I'd like to see this tested in multiple environments that I don't have access to, but I have no reason to believe that it wouldn't at least work everywhere that the current code works.

If you could review it carefully and add any comments, that would be good.

}
}

public enum SampleEnum
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is already within an internal class, so it's not really public.

This should work on Unity, Mono and .NET 3.5 as far as I'm aware.
It won't work on platforms where reflection itself is prohibited,
but that's a non-starter basically.
For oneofs, to get the case, we need to call the property that
returns the enum value. We really want it as an int, and modern
runtimes allow us to create a delegate which returns an int from the
method. (I suspect that the MS runtime has always allowed that.)
Old versions of Mono (e.g. used by Unity3d) don't allow that, so we
have to convert the enum value to an int via boxing. It's ugly, but
it should work.
@jskeet
Copy link
Contributor Author

jskeet commented Mar 27, 2018

Rebased and force-pushed to get it back up to date and rerun the tests.

Copy link
Contributor

@jtattermusch jtattermusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think overall things are making sense, but I requested clarification in one place.

{
// .NET Core (at least netstandard1.0) doesn't have Delegate.CreateDelegate, and .NET 3.5 doesn't have
// MethodInfo.CreateDelegate. Proxy from one to the other on .NET 3.5...
internal static class MethodInfoExtensions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: only put #if NET35 around the static class, not around the whole file? (I think otherwise the conditional compilation is easy to miss when reading the code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more logical sense for it to be for the whole file - otherwise on other targets the flie says "Here's a namespace declaration with no members being declared in it."

This matches what we've got in the other compatibility files, although in TypeExtensions it's after the using directives for no obvious reason.

}

// Public to make the reflection simpler.
public static SampleEnum SampleEnumMethod() => SampleEnum.X;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add EOL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, although I generally don't care about being consistent on that front.

return Expression.Lambda<Func<IMessage, object>>(upcast, parameter).Compile();
}
internal static Func<IMessage, object> CreateFuncIMessageObject(MethodInfo method) =>
GetReflectionHelper(method.DeclaringType, method.ReturnType).CreateFuncIMessageObject(method);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to understand the relationship between method.DeclaringType and IMessage here (and below).
If we are making a silent assumption that the methodInfo being passed needs to be of a method that is declared in a class that inherits from IMessage, we should make that more clear in the comment (looks like the method.DeclaringType is captured here as the concrete type of the protobuf message we are dealing with).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the comment "cast the argument to the appropriate method target type" is not accurate IMHO. It cast the argument to method's declaring type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten, and then copy/pasted the new text to other comment.s

Copy link
Contributor

@jtattermusch jtattermusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@jtattermusch jtattermusch merged commit 2537bea into protocolbuffers:master Mar 27, 2018
@jskeet
Copy link
Contributor Author

jskeet commented Mar 27, 2018

@anandolee @xfxyjwf any objections to me merging?

@xfxyjwf
Copy link
Contributor

xfxyjwf commented Mar 27, 2018

Thanks for doing this!

@SetoKaiba
Copy link

SetoKaiba commented Mar 28, 2018

EDIT: It's working now. Forgot to disable the strip for Google.Protobuf in linker.xml.
This is reported on Unity3D iOS. Any idea? @jskeet

System.TypeInitializationException: The type initializer for 'Co.Kaiba.Blueeyes.Protocol.BlueeyesBasicReflection' threw an exception. ---> System.TypeInitializationException: The type initializer for 'Google.Protobuf.Reflection.ReflectionUtil' threw an exception. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Google.Protobuf.Reflection.ReflectionUtil.CheckCanConvertEnumFuncToInt32Func () [0x00000] in <00000000000000000000000000000000>:0
at Google.Protobuf.Reflection.ReflectionUtil..cctor () [0x00000] in <00000000000000000000000000000000>:0
at Google.Protobuf.Reflection.GeneratedClrTypeInfo..ctor (System.Type clrType, Google.Protobuf.MessageParser parser, System.String[] propertyNames, System.String[] oneofNames, System.Type[] nestedEnums, Google.Protobuf.Reflection.GeneratedClrTypeInfo[] nestedTypes) [0x00000] in <00000000000000000000000000000000>:0
at Co.Kaiba.Blueeyes.Protocol.BlueeyesBasicReflection..cctor () [0x00000] in <00000000000000000000000000000000>:0
at Co.Kaiba.Blueeyes.Protocol.LoginRequest.get_Descriptor () [0x00000] in <00000000000000000000000000000000>:0
at BlueeyesBasicProtocolImpl.ChannelActive (DotNetty.Transport.Channels.IChannelHandlerContext context) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.DefaultChannelPipeline.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FulfillConnectPromise (System.Boolean wasActive) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FinishConnect (DotNetty.Transport.Channels.Sockets.SocketChannelAsyncOperation operation) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.RunAllTasks (DotNetty.Common.PreciseTimeSpan timeout) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.b__25_0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ScheduleAndStart (System.Boolean needsProtection) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.InternalStartNew (System.Threading.Tasks.Task creatingTask, System.Delegate action, System.Object state, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.Tasks.TaskCreationOptions options, System.Threading.Tasks.InternalTaskOptions internalOptions, System.Threading.StackCrawlMark& stackMark) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.TaskFactory.StartNew (System.Action action, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskCreationOptions creationOptions, System.Threading.Tasks.TaskScheduler scheduler) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.XThread+<>c__DisplayClass13_0.b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ThreadHelper.ThreadStart_Context (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
--- End of inner exception stack trace ---
at Google.Protobuf.Reflection.GeneratedClrTypeInfo..ctor (System.Type clrType, Google.Protobuf.MessageParser parser, System.String[] propertyNames, System.String[] oneofNames, System.Type[] nestedEnums, Google.Protobuf.Reflection.GeneratedClrTypeInfo[] nestedTypes) [0x00000] in <00000000000000000000000000000000>:0
at Co.Kaiba.Blueeyes.Protocol.BlueeyesBasicReflection..cctor () [0x00000] in <00000000000000000000000000000000>:0
at Co.Kaiba.Blueeyes.Protocol.LoginRequest.get_Descriptor () [0x00000] in <00000000000000000000000000000000>:0
at BlueeyesBasicProtocolImpl.ChannelActive (DotNetty.Transport.Channels.IChannelHandlerContext context) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.DefaultChannelPipeline.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FulfillConnectPromise (System.Boolean wasActive) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FinishConnect (DotNetty.Transport.Channels.Sockets.SocketChannelAsyncOperation operation) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.RunAllTasks (DotNetty.Common.PreciseTimeSpan timeout) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.b__25_0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ScheduleAndStart (System.Boolean needsProtection) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.InternalStartNew (System.Threading.Tasks.Task creatingTask, System.Delegate action, System.Object state, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.Tasks.TaskCreationOptions options, System.Threading.Tasks.InternalTaskOptions internalOptions, System.Threading.StackCrawlMark& stackMark) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.TaskFactory.StartNew (System.Action action, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskCreationOptions creationOptions, System.Threading.Tasks.TaskScheduler scheduler) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.XThread+<>c__DisplayClass13_0.b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ThreadHelper.ThreadStart_Context (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
--- End of inner exception stack trace ---
at Co.Kaiba.Blueeyes.Protocol.LoginRequest.get_Descriptor () [0x00000] in <00000000000000000000000000000000>:0
at BlueeyesBasicProtocolImpl.ChannelActive (DotNetty.Transport.Channels.IChannelHandlerContext context) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.InvokeChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.AbstractChannelHandlerContext.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.DefaultChannelPipeline.FireChannelActive () [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FulfillConnectPromise (System.Boolean wasActive) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Transport.Channels.Sockets.AbstractSocketChannel+AbstractSocketUnsafe.FinishConnect (DotNetty.Transport.Channels.Sockets.SocketChannelAsyncOperation operation) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.RunAllTasks (DotNetty.Common.PreciseTimeSpan timeout) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.SingleThreadEventExecutor.b__25_0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ScheduleAndStart (System.Boolean needsProtection) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.InternalStartNew (System.Threading.Tasks.Task creatingTask, System.Delegate action, System.Object state, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.Tasks.TaskCreationOptions options, System.Threading.Tasks.InternalTaskOptions internalOptions, System.Threading.StackCrawlMark& stackMark) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.TaskFactory.StartNew (System.Action action, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskCreationOptions creationOptions, System.Threading.Tasks.TaskScheduler scheduler) [0x00000] in <00000000000000000000000000000000>:0
at DotNetty.Common.Concurrency.XThread+<>c__DisplayClass13_0.b__0 () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.Execute () [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteWithThreadLocal (System.Threading.Tasks.Task& currentTaskSlot) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.Tasks.Task.ExecuteEntry (System.Boolean bPreventDoubleExecution) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ThreadHelper.ThreadStart_Context (System.Object state) [0x00000] in <00000000000000000000000000000000>:0
at System.Threading.ExecutionContext.RunInternal (System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, System.Object state, System.Boolean preserveSyncCtx) [0x00000] in <00000000000000000000000000000000>:0
DotNetty.Transport.Channels.AbstractChannelHandlerContext:InvokeChannelActive()
DotNetty.Transport.Channels.AbstractChannelHandlerContext:FireChannelActive()
DotNetty.Transport.Channels.AbstractChannelHandlerContext:InvokeChannelActive()
DotNetty.Transport.Channels.AbstractChannelHandlerContext:FireChannelActive()
DotNetty.Transport.Channels.AbstractChannelHandlerContext:InvokeChannelActive()
DotNetty.Transport.Channels.AbstractChannelHandlerContext:FireChannelActive()
DotNetty.Transport.Channels.DefaultChannelPipeline:FireChannelActive()
DotNetty.Transport.Channels.Sockets.AbstractSocketUnsafe:FulfillConnectPromise(Boolean)
DotNetty.Transport.Channels.Sockets.AbstractSocketUnsafe:FinishConnect(SocketChannelAsyncOperation)
DotNetty.Common.Concurrency.SingleThreadEventExecutor:RunAllTasks(PreciseTimeSpan)
DotNetty.Common.Concurrency.SingleThreadEventExecutor:b__25_0()
System.Threading.Tasks.Task:Execute()
System.Threading.ExecutionContext:RunInternal(ExecutionContext, ContextCallback, Object, Boolean)
System.Threading.Tasks.Task:ExecuteWithThreadLocal(Task&)
System.Threading.Tasks.Task:ExecuteEntry(Boolean)
System.Threading.Tasks.Task:ScheduleAndStart(Boolean)
System.Threading.Tasks.Task:InternalStartNew(Task, Delegate, Object, CancellationToken, TaskScheduler, TaskCreationOptions, InternalTaskOptions, StackCrawlMark&)
System.Threading.Tasks.TaskFactory:StartNew(Action, CancellationToken, TaskCreationOptions, TaskScheduler)
DotNetty.Common.Concurrency.<>c__DisplayClass13_0:b__0()
System.Threading.Tasks.Task:Execute()
System.Threading.ExecutionContext:RunInternal(ExecutionContext, ContextCallback, Object, Boolean)
System.Threading.Tasks.Task:ExecuteWithThreadLocal(Task&)
System.Threading.Tasks.Task:ExecuteEntry(Boolean)
System.Threading.ThreadHelper:ThreadStart_Context(Object)
System.Threading.ExecutionContext:RunInternal(ExecutionContext, ContextCallback, Object, Boolean)

(Filename: /Users/builduser/buildslave/unity/build/Runtime/Export/Debug.bindings.h Line: 43)

@jskeet
Copy link
Contributor Author

jskeet commented Mar 29, 2018

Thanks for letting me know - I'll have a look as soon as I can.

@SetoKaiba
Copy link

@jskeet No need to have a look now. Just check my edited comment. It's working now. I forgot to add the Google.Protobuf to linker to disable the strip. After I add it, it's working.

@jskeet
Copy link
Contributor Author

jskeet commented Mar 29, 2018

@SetoKaiba: That's great to know, although it would be nice to be able to avoid that being required.
I have an idea for how I could do that - how easy would it be for you to test a new PR in Unity3D/iOS? (And can you check that JSON formatting works? Just calling ToString on a simple message should be enough - it would have failed before this PR.)

@ObsidianMinor
Copy link
Contributor

ObsidianMinor commented Apr 2, 2018

@SetoKaiba are you running on a platform that requires AOT compilation or no?

@SetoKaiba
Copy link

@ObsdianMinor of course requiring. ios il2cpp

@5argon
Copy link

5argon commented Apr 6, 2018

Just a small question, but do you know what kind of common function besides .ToString() needs the Descriptor (and in turn uses reflection)?

For now I would like to have a dirty fix to avoid those functions for the time being. For example if I want to just get a string representation, I can just dot the object to get the value and then ToString that instead of ToString the whole Protobuf object which it must consult the descriptor. If it is just ToString I might not need the fix anymore and just avoid it entirely.

https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor

@jskeet
Copy link
Contributor Author

jskeet commented Apr 6, 2018

@5argon: Anything that wants to use reflection, basically. That's JSON parsing, creating a string representation, and anything your code might want to do with reflection itself.

I'm not sure whether you're asking for us to create a "dirty fix" but I definitely don't want to spend any time finding a workaround when instead time needs to be spent understanding this properly. And regardless of whether you need string handling or not, other developers certainly will.

All that being said, we still don't officially support Unity yet, so I'm not making any guarantees about what will happen.

@5argon
Copy link

5argon commented Apr 6, 2018

Hello @jskeet , sorry I made a mistype. (Not a native English speaker here) It's "I will just use a" instead of "I would like to have". Reading it again I am not sure why I typed that. Sorry that it comes out as offensive.

That is to say for the time being I will just use a dirty fix for my own project, which is just avoid the funtions that uses the reflection until Unity correctly prevent the constructor to be stripped off.

In that post I wanted to know is there anything else that I might come across so when I code I could avoid using them instead of encountering them at runtime. (I even thought ToString would enumerate all the variables in the generated code at first, I had no clue until the error comes up that it uses JSON) But per your reply if it is just JSON parsing and string representation then it is already very usable in Unity.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 6, 2018

@5argon: No problem - I'm sorry for getting so defensive :)

I'm still surprised that this isn't fixable with linker configuration though. I definitely want to fix it properly; it's just a matter of finding time :(

@5argon
Copy link

5argon commented Apr 10, 2018

Earlier Unity team returned my report that they have been able to reproduce this "bug" that link.xml fails to prevent code stripping. Here's the link to track the bug report progress.

https://fogbugz.unity3d.com/default.asp?1020952_kjitf02otf9p95rh

@jskeet
Copy link
Contributor Author

jskeet commented Apr 10, 2018

@5argon: That's really good to know. It'll be interesting to see what they find out.

@5argon
Copy link

5argon commented Apr 17, 2018

They have replied :

Thanks for reporting this issue. I believe that the problem is unrelated to managed code stripping or a link.xml file. Instead, the problem is an ahead-of-time (AOT) compiler limitation. IL2CPP is unable to generate code for all possible combinations of generic types that have value types as arguments.

In this case, the problem seems to be with the type Google.Protobuf.Reflection.ReflectionUtil.ReflectionHelper<AbsolutePosition, int>, where int is the value type causing the issue here.

Unfortunately, I don't see a good way to work around this issue, but I'm pretty unfamiliar with the protobuf code, so hopefully I'm missing something. We need to tell IL2CPP about the type it will need to emit code for. We could do that with a line of code like this in the ReflectionTest method:

var test = Google.Protobuf.Reflection.ReflectionUtil.ReflectionHelper<AbsolutePosition, int>();

However, ReflectionUtil is in internal type in the protobuf assembly, so we can't access it. After inspecting the code in the protobuf assembly with ILSpy, I noticed a method named PreventLinkerFailures in the ReflectionUtil type. That method seems to be trying to work around an issue like this by creating an unused new instance of the type ReflectionUtil.ReflectionHelper<int, int>.

So it might be possible to either build the protobuf assembly with knowledge of the AbsolutePosition type from your project so that it will have this special code to help the AOT compiler. Or, you could try to represent the data for AbsolutePosition as one or more int values. I suspect they will work with ReflectionUtil.ReflectionHelper and IL2CPP properly.

Longer term, we're actively working on a new feature for IL2CPP to allow it to avoid this AOT restriction, and generate proper code in this case. However, that feature is not ready to ship.

Please let me know if you have any questions about these issues. Hopefully we can find a work around.

Josh Peterson
Developer - Platform Foundation Team

Seems like IL2CPP needs knowledge about your custom protobuf class in the library code (Because of internal) for the AOT compiler to emit code. This is why the dummy ReflectionHelper<int, int> instantiating does not help with this.

And also it is irrelevant to code stripping. link.xml prevents stripping, but the code wasn't there in the first place. (But now I am not sure what makes @SetoKaiba .ToString() code works where it could have been the same problem.)

ps. AbsolutePosition is my own generated protobuf class with just one int.

@SetoKaiba
Copy link

SetoKaiba commented Apr 17, 2018

I'm just curious why my code does has relevant code generation while yours doesn't.
My protobuf class is even more complex than yours.

EDIT: That's why I insist It's a bug jskeet should have a look at it instead of Unity in the later replies.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 17, 2018

@SetoKaiba "That's why I insist It's a bug" - well it's a problem with the Unity AOT compiler basically violating the expectations of normal .NET code. This kind of thing is precisely why I've been reluctant to support Unity from the start. While it would be nice to have, trying to operate in an environment where anything can go wrong in hard-to-predict ways makes it very hard to get anything done.

Given that there can be any number of different enums involved, I'm not sure there's anything we can do here - the workaround helps in some situations but not others. I suspect that users will have to accept a restriction of "no reflection in Unity on IL2CPP platforms" until the workaround from Unity ships.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 17, 2018

@5argon: Thanks for including all of that detail. I'll get in touch with Unity to see if there are any options, but it really depends on exactly what combinations are problematic. If every enum counts as a different value type, there's no hope. If they treat all enums as basically being the same, we may have a chance, but I don't know when I'm going to have time to work on this.

@SetoKaiba
Copy link

@jskeet First of all, I'm not blaming. I mean that the code for me doesn't strip the code generation for my protobuf classes. That's what I'm curious. Maybe it's something particular with his code.
My code is working great in my case. I use protobuf all around for my networking protocol. And also you asked me to print the ToString() of my protobuf class. It does work.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 17, 2018

@SetoKaiba: I suspect that we'd need to know a lot of details about the exact configuration and versions of all kinds of things in order to work out why it works in one case but not another. That would certainly be a useful part of the next step in diagnosing the problem. But I'm not really in a position to investigate it further, not being a Unity developer or having protobuf as my main project. (The aim was always that I would work on the original C# implementation then move off the project. I can still do bits and pieces here and there, but not the sort of large-scale work that I believe this will require.)

@5argon
Copy link

5argon commented Apr 17, 2018

@jskeet He did say

Longer term, we're actively working on a new feature for IL2CPP to allow it to avoid this AOT restriction, and generate proper code in this case. However, that feature is not ready to ship.

So personally I think it is the best that we don't do anything at all.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 17, 2018

@5argon: Waiting for the situation to resolve itself is certainly the simplest option, although it won't be as helpful for developers using the existing versions of Unity. I've reached out by email so I can find out more and do a little exploratory work, but we'll see what happens.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 26, 2018

Good news: thanks to some great help from the Unity IL2CPP team, I've managed to create a repro for this with a simple console app, which will make it much easier to investigate. It's not yet clear what that will mean in terms of an end result, but I'm in a much better position to diagnose and test it.

@SetoKaiba
Copy link

SetoKaiba commented Apr 26, 2018

Great news. I found that I encountered 5argon's problem on other protobuf messages as well.

@jskeet
Copy link
Contributor Author

jskeet commented Apr 26, 2018

@5argon @SetoKaiba if either or both of you are in a position to try out a new pull request, see #4559

Note that you'll still need a small amount of extra code in your application, e.g.

// All generated enums should be listed here
FileDescriptor.ForceReflectionInitialization<Foo>();
FileDescriptor.ForceReflectionInitialization<Bar>();

Note that you need to list all the enums you're using from any single source proto where you're using reflection with any of the types from that proto. That includes nested enums and the enums generated for one-of values.

In the future we could potentially generate that along with the rest of the proto code, or I could write a small C# tool that would load an assembly and generate a single source file with all the enums for assemblies specified on the command line. But let's make sure it works first :)

I believe you should then be okay without any other linker options.

@SetoKaiba
Copy link

@jskeet Thank you for your great job. I'll test it later today.

@SetoKaiba
Copy link

@jskeet Sorry for a late reply. Being busy before. It's working great with the latest merged master branch.

@jskeet
Copy link
Contributor Author

jskeet commented May 20, 2018

@SetoKaiba: Excellent - glad it works. We can look into generating all the relevant code in the future, but unblocking everyone is at least a start :)

@unitymatrix
Copy link

while still exist such problem with protobuf 3.6.1

@unitymatrix
Copy link

I have modify the protobuf code under .net framework 3.5,build xcode project in IL2CPP mode
while the problem is still there

@unitymatrix
Copy link

I've debuged here
public static void ForceReflectionInitialization<T>() { UBase.UDebug.LogError("run here=========:"); ReflectionUtil.ForceInitialize<T>(); }
strangely that the code have run.

@jskeet
Copy link
Contributor Author

jskeet commented Aug 3, 2018

@unitymatrix: I'm afraid I don't fully understand your comments. But please note that this is only part of a workaround - in order to make this work at the moment, you have to call FileDescriptor.ForceReflectionInitialization<MyEnum>() for every enum in any proto file where you want to use reflection. That code isn't generated at the moment, and we're hoping that IL2CPP will be updated to make it unnecessary, but it should be reasonably simple.

With those calls in place, it should be fine. If it isn't, please provide more detail, ideally in a complete example I could use to reproduce the problem.

zhoutwo pushed a commit to zhoutwo/protobuf that referenced this pull request Apr 25, 2020
Change C# reflection to avoid using expression trees
@Moe-Baker
Copy link

Moe-Baker commented Aug 15, 2020

Boy! this was one hell of a read.
Can't wait to see if Unity is still going to fail on me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants