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

Add null checks to GetOption #7130

Conversation

ObsidianMinor
Copy link
Contributor

Fixes #7127

Null continues to erode away at my soul.

Should we add a TryGetOption API in another PR to better match up with the CustomOptions API @jtattermusch?

Copy link

@thesoftwarejedi thesoftwarejedi left a comment

Choose a reason for hiding this comment

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

Using default(T) works fine for our use cases.

@jtattermusch jtattermusch self-assigned this Feb 28, 2020
@Falco20019
Copy link
Contributor

I also just ran into that problem and would like the idea of having TryGetOption. I'm iterating over some fields and have to check if any has the option set. This is not possible with only using default(T).

@thesoftwarejedi
Copy link

@Falco20019 I suggest we merge this, but also add the Try versions of all methods in another PR. I can try to get to coding that this weekend using #7240 as the basis for it.

I'm not familiar with the release pipeline here or how to push this along. I think @ObsidianMinor is king.

@ObsidianMinor
Copy link
Contributor Author

@thesoftwarejedi I'm working on a TryGet PR right now. Also I don't work at Google, I'm just a friendly OSS contributor. Ask @jtattermusch.

@thesoftwarejedi
Copy link

@thesoftwarejedi I'm working on a TryGet PR right now.

Perfect, thanks! Saved my weekend ;)

Also I don't work at Google, I'm just a friendly OSS contributor.

I figured as much. Thanks for your contributions!

@ozevren
Copy link

ozevren commented Mar 13, 2020

Is there any way I can help with getting this checked in? This and the Try overload our blocking some of my, so I am highly motivated to help out here. :)

var options = Proto.Options;
if (options == null)
{
return default(T);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think returning default(T) is a good idea. It seems that for e.g. extension of type int, you won't be able to differentiate between the option not being present at all (which should return null) and the option having value of 0.

Given that the GetOption API is relatively new (and looks like it has been kind of broken for the very beginning), should we just introduce TryGetOption methods instead, to keep the semantics clean?

the GetOption can either be removed (that should be fine because it was only introduced as part of the proto2 work and we've marked proto2 support as "experimental") or it can just throw an exception if the value is not present (which renders it not very useful, so we might wanna mark it as obsolete?).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Returning default(T) is a mistake. It should be the default value for the extension.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still, even if you return extension.DefaultValue, it is unclear to me how I would distinguish between 1. the option not being present at all and 2. the option being present and being set to the default value (AFAIK options/extensions don't need to have default values).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can't, which is why #7282 was made. Should we also add a HasOption API to go with it?

@@ -184,13 +184,21 @@ public void NoOptions()
var fileDescriptor = UnittestProto3Reflection.Descriptor;
var messageDescriptor = TestAllTypes.Descriptor;
Assert.NotNull(fileDescriptor.CustomOptions);
Assert.AreEqual(fileDescriptor.GetOption(Fileopt), null);
Copy link
Contributor

Choose a reason for hiding this comment

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

why are we intermixing the GetOption tests with a CustomOptions-related test (and all that in CustomOptionsTest.cs).
I think it would be better if GetOption methods was tested in a separate test case.

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 thought that even though the GetOption APIs weren't in a "CustomOptions" type they still worked with "custom options" so it'd be easier to test everything relating to custom options together.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm probably fine keeping the test in the same file (CustomOptionsTest.cs), but we should at least put them in a separate test case (method) as I proposed. I think like this the test will be pretty hard to maintain and refactor
(and frankly it's not clear to me why Assert.NotNull(messageDescriptor.Fields[1].CustomOptions); and Assert.AreEqual(messageDescriptor.Fields[1].GetOption(Fieldopt), null); would be related.

@Falco20019
Copy link
Contributor

Wouldn’t it be nicer if Proto.Options would not be null but always defined empty and that GetExtension would then just return the DefaultValue of the extension requested? This would avoid a lot of code duplication.

@@ -555,7 +555,13 @@ public override string ToString()
/// </summary>
public T GetOption<T>(Extension<FileOptions, T> extension)
{
var value = Proto.Options.GetExtension(extension);
var options = Proto.Options;
if (options == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

actually, looking at the whole options API in protobuf C# from a more high level perspective, we only need to check whether options == null because e.g. the FileDescriptor doesn't provide access to the file options (the property FileDescriptor.Proto is internal, so one cannot access to FileDescriptor.Proto.Options).

This actually has several downsides:

  • we need extra logic such as a GetOption methods, instead of just exposing e.g. the FileOptions msg (or other options) on a descriptor and letting the user use GetExtension methods the same way they would do it for any other message with extensions (which is the way reading custom options is done for e.g. C++, Java and python, as documented here: https://developers.google.com/protocol-buffers/docs/proto#customoptions)
  • because the "options" messages are not exposed, one cannot access the standard (=the non-extension ones) options at all.

This makes me think that the whole GetOptions() and TryGetOption() API is a bad idea and we should rather focus on how to expose the descriptor.Options property safely, because then we wouldn't need to worry about the GetOptions/TryGetOptions etc. API's at all (they could be removed and everyone could just rely on standard extension APIs).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only way we could expose descriptor.Options safely is by deep cloning it (since all the protos are supposed to be immutable). With these APIs the only thing we need to clone is the value, if we need to clone it at all.

C++ has const references, Java has readers, and Python parses a local options value (but allows mutation).

Maybe we could do it like Python but, again, we'd still clone the whole options message once for every unique descriptor on get.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe the deep clone isn't really all that problematic. Most projects would only have a few options in place and it seems fair to require users to cache the results they got from e.g. messageDescriptor.Options if they wanted to process a large number of options.
It seems to me that just exposing description.Options (as a defensive copy) would lead to a more logical and straightforward API (with smaller footprint and fewer duplications, and also more similar to what other languages do) - WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could we make a internal wrapper class that exposes the IExtendableMessage API but only implements the pure functions? Then GetOptions would just return an IExtendableMessage implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

But you actually want to be able to access e.g. the FileOptions' properties (the "standard" options, such as "java_package" etc.)

see

@jtattermusch
Copy link
Contributor

Superseded by #7434 (GetOption API is now obsolete).

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.

C#: MethodDescriptor.GetOption throws NullReferenceException
8 participants