-
Notifications
You must be signed in to change notification settings - Fork 15.5k
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
Add support for proto3 optional fields in C# #7382
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the speedy review. Will revert the first commit and use real_oneof_decl_count etc to simplify the second commit, then force push.
Changes now back in - it was nice to remove the first commit and the frequent "is_synthetic" checks. (It looks like some of the rest of the code could be tidied up by using real_containing_oneof and real_oneof_decl_count too, but I haven't tried to do that here.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the next iteration - will see when I can find time to make the proposed changes.
I'll probably do those in a separate commit for now so it's easier to review, then rebase before merging. (The previous changes were sufficiently pervasive that it wasn't worth having them as extra commits.)
Ah, I think I understand the quandry we are in then. It sounds like proto2 and proto3 are currently differing on this point? Proto2 returns a default instance for an unset message field, while proto3 returns That means we have to choose our inconsistency. Either:
I'm not sure yet what the right path is here. |
No, both return null - it's just that Proto2 still has protobuf/csharp/src/Google.Protobuf.Test.TestProtos/TestMessagesProto2.cs Lines 927 to 942 in 7b8a241
If we're content to break existing proto2 customers, we could remove those - I don't think they add value. At that point we could be consistent between proto2, proto3-implicit-optional and proto3-explicit-optional. |
It sounds like the right thing to do for this PR is to omit presence for message fields. That's very easy to do. |
I've added two more commits - one with source code changes addressing your comments (hopefully - I may have missed some long lines, so please let me know if there's anything there), and another with the result of regenerating for comparison. I would expect to fold these into the existing commits later - let me know when you want me to (or if you'd rather I didn't). |
By "omit presence" do you just mean omitting the That seems like a fine first step. :) We would generally say that message fields are still tracking presence, since |
Yes, I just mean removing the Sounds like we're on the same page - so I suspect the next step is writing unit tests for the code generated from unittest_proto3_optional.proto, and doing whatever's required for the reflection API. At that point I believe this PR would be ready to merge, and then we can consider the other suggestions that have come up in the review. |
Sounds great, thanks! :) |
Okay, I believe this is now ready for review with an eye to merging. |
What I was thinking with HasFoo and ClearFoo for nullable types was I wanted the API to stay the same for all fields no matter how their presence is stored to keep everything consistent with both proto2 non-nullable fields and proto3 nullable fields, since proto3 nullable fields don't accept null. I thought it may be confusing if some messages had nullable fields that took null and other messages had nullable fields that did not. Plus if consumers moved from proto2 to proto3 those field setters would silently change behavior rather than just HasFoo and ClearFoo no longer existing and consumers having to adjust accordingly. |
I don't know what you mean by that. The wrapper type fields are represented by nullable value types, and you can set them to null. Message type fields are represented by references, and you can set them to null. It's only Given that the Has/Clear members for message type fields just check/set null references, and that's easy to do in C# already, I'd rather not have them in either proto2 or proto3. It's already quite confusing looking through the reference documentation for a generated C# class given how many members there are - having three members for every message field, without providing any usability benefit IMO, seems like a bad idea. I don't see what you mean about field setters silently changing behavior - could you clarify what you mean by that? (If you can give a concrete example of a proto and what you understand the behavior to be before and after, that would be easiest.) |
Ah, it's been a while. You're right if messages don't already do null-checks then that was a waste of API space and a mistake on my part. I believe when proto2 support was released we mentioned that all the API was experimental so I think it might be ok to remove those properties and methods. |
Great. I've filed #7395 I'd suggest only looking at implementing it after this is in, as then it'll just be a couple of lines of code, I believe. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I performed review for the C# portion of the code and things are looking good to me (with a few very minor nits).
throw new InvalidOperationException("HasValue is not implemented for proto3 fields"); | ||
hasDelegate = message => | ||
{ | ||
throw new InvalidOperationException("HasValue is not implemented for non-optional proto3 fields"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/implemented/supported/ - "not implemented" sounds like something that should be implemented, but isn't yet. "Not supported" sounds like this is by design. Leaving up to you whether you want to change this or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I really don't have strong feelings one way or another - I tend to lean towards "keep things as they are" when I'm on the fence.
@@ -48,13 +48,16 @@ namespace csharp { | |||
// header. If you create your own protocol compiler binary and you want | |||
// it to support C# output, you can do so by registering an instance of this | |||
// CodeGenerator with the CommandLineInterface in your main() function. | |||
class PROTOC_EXPORT Generator : public CodeGenerator { | |||
class PROTOC_EXPORT CSharpGenerator : public CodeGenerator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just curious: why rename the class? It's under the csharp namespace already, so strictly speaking this shouldn't be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it for consistency with the other classes I was looking at - namely JavaGenerator and CppGenerator. Looking at PHP, Ruby, and Python though, they just have Generator
.
@haberman any preference on this?
@haberman: Other than the question about renaming the generator class, do you have any further requested changes? Looks like the rename might have been the cause of the Kokoro build failing - I'll wait for comments about that before changing anything, as either I'll revert the rename, or update the tests. |
@jskeet Naming of the code generator class is totally up to you. :) There aren't really any compatibility implications there to worry about. What was the final outcome of the discussion about API?
Those are the two issues I'm the most focused on. I want the answers to both of these to be "no." :) It sounds like your plan is to merge this PR, then remove hazzers from message fields in proto2 per #7395? That seems like a fine resolution. If you are always representing non-present messages with |
In that case I'll revert that part of the change then. Given that there's no need for the class name to change, and it's as negative as it's positive, let's avoid the churn :)
Yes, at the moment.
No.
Exactly - assuming we're willing to make a breaking change to the proto2 generated code. |
(It doesn't yet, but will in the next commits...)
Most changes are: - Introducing new helpers of SupportsPresenceApi and RequiresPresenceBit. This allows calling code to be a lot clearer about what it's interested in. - Changing most previous IsProto2 calls to use one of the two new helper methods - Avoiding treating synthetic oneofs as regular ones - Some slight refactoring in csharp_primitive_field to avoid code duplication - Comments explaining what we want when, so the next maintainer doesn't need to do the detective work I did! This change deliberately doesn't modify the API surface of any existing code. The only change to previously-generated C# should be making presence bits more efficient in proto2. Once proto3 optional fields are supported, we can consider further changes to make the proto2 and proto3 generated API surface more consistent (e.g. adding presence API for message fields and oneofs).
…ional.proto The changes in the existing proto2 code are solely around presence bits. The new generator allocated presence bits more efficiently. (Previously bits were sometimes allocated but never used.)
(This isn't as exhaustive as it might be, but the behavior is basically the same as proto2 optional fields.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments.
Also, would it make sense to expose accessors like C++ does for real_containing_oneof()
, real_oneof_count()
etc.? Does C# support loading descriptors at runtime? If so we should probably enforce that synthetic oneofs are last, like in C++.
/// <summary> | ||
/// Returns <c>true</c> if this field is a proto3 optional field; <c>false</c> otherwise. | ||
/// </summary> | ||
public bool IsProto3Optional => Proto.Proto3Optional; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to expose this publicly. I think we paint ourselves into a corner if code is written to think about the difference between proto2 and proto3.
Could you follow the pattern of the C++ API? C++ is hiding this accessor, and instead exposing a more semantically meaningful accessor: has_presence()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to remove it from the public API for now, but I'll need to check what exact semantics we want for HasPresence. (In particular, it may well not be "if there a HasFoo property".) I'm not going to speculate on it late at night my time :)
@@ -59,7 +59,8 @@ public interface IFieldAccessor | |||
object GetValue(IMessage message); | |||
|
|||
/// <summary> | |||
/// Indicates whether the field in the specified message is set. For proto3 fields, this throws an <see cref="InvalidOperationException"/> | |||
/// Indicates whether the field in the specified message is set. | |||
/// For proto3 fields that aren't explicitly optional, this throws an <see cref="InvalidOperationException"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect this to work for oneof fields also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could do, but it didn't before this PR - I'd prefer to treat that as a separate feature to implement. Likewise we can do it for message fields whether or not they're explicitly optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, would it make sense to expose accessors like C++ does for real_containing_oneof(), real_oneof_count() etc.? Does C# support loading descriptors at runtime? If so we should probably enforce that synthetic oneofs are last, like in C++.
Yes, we can load descriptors at execution time. I think I'll want to do the extra oneof work in a separate commit. Will look into it tomorrow, but I may not have time to implement it this week. (It's probably worth mentioning this as a requirement in the internal implementation guide as well.)
/// <summary> | ||
/// Returns <c>true</c> if this field is a proto3 optional field; <c>false</c> otherwise. | ||
/// </summary> | ||
public bool IsProto3Optional => Proto.Proto3Optional; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to remove it from the public API for now, but I'll need to check what exact semantics we want for HasPresence. (In particular, it may well not be "if there a HasFoo property".) I'm not going to speculate on it late at night my time :)
@@ -59,7 +59,8 @@ public interface IFieldAccessor | |||
object GetValue(IMessage message); | |||
|
|||
/// <summary> | |||
/// Indicates whether the field in the specified message is set. For proto3 fields, this throws an <see cref="InvalidOperationException"/> | |||
/// Indicates whether the field in the specified message is set. | |||
/// For proto3 fields that aren't explicitly optional, this throws an <see cref="InvalidOperationException"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could do, but it didn't before this PR - I'd prefer to treat that as a separate feature to implement. Likewise we can do it for message fields whether or not they're explicitly optional.
Yep that is fair. I think the best semantics for
Yep, agree on both counts (message fields should always work here, and it seems fine to implement this separately in a follow-up PR).
Good point, I will add this to: https://github.com/protocolbuffers/protobuf/blob/9ae5203712eb80f89261d6df8d5674efa5a0edb8/docs/implementing_proto3_presence.md |
@haberman: Please see the last commit for reflection changes. I'm pretty comfortable with those. Just to confirm, I'm then expecting to perform follow-up PRs of:
Does that all sound correct to you? |
Thanks - will fix up the commits to be a bit more pleasant, and investigate the Kokoro build failures, then merge on green. |
This is more involved than might be expected because the synthetic oneofs don't generate the properties we would usually expect to see.
Yep, sounds great. :) |
This is the generator part of the C# proto3 optional fields work.
Still to do: