Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Proposal: Safe pointers #1043

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

Merged
merged 9 commits into from
Feb 26, 2020
Merged

Conversation

lostmsu
Copy link
Member

@lostmsu lostmsu commented Feb 13, 2020

What does this implement/fix? Explain your changes.

With C# 7.3 ref structs and C# 8.0 using working on any method named Dispose it is possible to track new vs borrowed references at compile time.

This proposal introduces BorrowedReference and NewReference types, that are ref-only structs, meaning you can't put them into PyObject directly, so they can implement different ToPyObject methods, one doing IncRef, and the other one not (alternatively, we can have PyObject constructor overloads). Along with Roslyn-based NonCopyableAnalyzer and FxCop after dotnet/roslyn-analyzers#3305 is fixed, this can ensure, that we do reference counting correctly at compile time.

We can gradually change return and parameter types in PInvoke methods in Runtime to one of BorrowedReference or NewReference to take advantage of this.

Downsides

Build system must support C# 8.0, which is currently not supported in all build configurations.
Without C# 8.0 one would have to dispose NewReference instances with try ... finally ... block.

@lostmsu lostmsu added the rfc label Feb 13, 2020
@@ -292,7 +292,7 @@ internal Binding Bind(IntPtr inst, IntPtr args, IntPtr kw, MethodBase info, Meth
for (int i = 0; i < pynkwargs; ++i)
{
var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist, i));
kwargDict[keyStr] = Runtime.PyList_GetItem(valueList, i);
kwargDict[keyStr] = Runtime.PyList_GetItem(valueList, i).DangerousGetAddress();
Copy link
Member Author

Choose a reason for hiding this comment

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

This generates a compiler warning, that uncovers a potentially dangerous pattern, where kwargsDict may contain borrowed references.

@@ -139,12 +139,13 @@ public PyObject Values()
/// </remarks>
public PyObject Items()
{
IntPtr items = Runtime.PyDict_Items(obj);
if (items == IntPtr.Zero)
using var items = Runtime.PyDict_Items(this.obj);
Copy link
Member Author

Choose a reason for hiding this comment

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

After dotnet/roslyn-analyzers#3305 is fixed we can configure build to fail here if using would be omitted

@lostmsu lostmsu requested a review from filmor February 14, 2020 00:05
@lostmsu
Copy link
Member Author

lostmsu commented Feb 14, 2020

@amos402 can you check this one out too?

@codecov-io
Copy link

codecov-io commented Feb 14, 2020

Codecov Report

Merging #1043 into master will increase coverage by 3.64%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1043      +/-   ##
==========================================
+ Coverage   83.11%   86.75%   +3.64%     
==========================================
  Files           1        1              
  Lines         302      302              
==========================================
+ Hits          251      262      +11     
+ Misses         51       40      -11
Flag Coverage Δ
#setup_linux 65.56% <ø> (ø) ⬆️
#setup_windows 71.52% <ø> (+6.62%) ⬆️
Impacted Files Coverage Δ
setup.py 86.75% <0%> (+3.64%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f5548e3...6ae63cd. Read the comment docs.

@amos402
Copy link
Member

amos402 commented Feb 14, 2020

You can also implement an implicit operator for PyObject make it easy for usage.
Reference struct also can provide more interfaces, like RefCnt, ToString, ect.
By the by, Py.GILState can be strcuct easily too.

@lostmsu lostmsu mentioned this pull request Feb 20, 2020
@lostmsu
Copy link
Member Author

lostmsu commented Feb 22, 2020

@filmor I'd love if we get this in and take as new standard for coding Python handle manipulations, as it would make otherwise hard-to-debug segmentation fault/access violation bugs easier to prevent/spot/debug.

@lostmsu
Copy link
Member Author

lostmsu commented Feb 22, 2020

@amos402 I can proceed to merge it, if you can sign-off a review, and are for this change in general.

I do not plan to extend the scope yet (like making PyObject changes to take these, etc). We can do it in the future PRs.

@@ -83,6 +83,7 @@
</Compile>
<Compile Include="arrayobject.cs" />
<Compile Include="assemblymanager.cs" />
<Compile Include="BorrowedReference.cs" />
Copy link
Member

Choose a reason for hiding this comment

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

Is it good for separate these three items to different files? They're small, related, and it make the project files be scattered.

Copy link
Member Author

Choose a reason for hiding this comment

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

The might get more code later on. Generally, I prefer a type per file approach for anything except delegate types.

Copy link
Member

Choose a reason for hiding this comment

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

Or create a folder for them? Just just too far if they sorted by name I thought🤣

{
using System;
[NonCopyable]
ref struct NewReference
Copy link
Member

Choose a reason for hiding this comment

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

Is it necessary to limit it as a stack-allocated value? If make it to a normal struct and rename it to PyReference, it can be assigned to a collection and implement the IDisposable for using () usage.
For better usage, a BorrowedReference may be able to promote to a PyReference for using the implict operate overloading.

Copy link
Member Author

Choose a reason for hiding this comment

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

With [NoCopyable] you won't be able to put it into a collection anyway. And without [NoCopyable] it does not provide any safety guarantees.

Having it as ref struct saves you from adding ref everywhere you take it.

Copy link
Member

Choose a reason for hiding this comment

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

But....sometimes I just want to put them into collections...😂

Copy link
Member Author

Choose a reason for hiding this comment

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

Then you can still do DangerousGetHandle or create a PyObject from them.

Copy link
Member

Choose a reason for hiding this comment

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

But this type also has a purpose for reminding others that it's a reference don't forget to decref it, isn't it?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it must be clear from code, that care is necessary around this scenario.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, as mentioned above, [NoCopyable] would already prevent you from putting an instance into a collection, even if ref were removed. And [NoCopyable] is basically the sole purpose of this change, as if you do

NewReference refA = GetNewReferenceSomehow();
NewReference refB = refA;

Then you already miappropriated refcounts.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe more introduce a type named PyHandle for that?

Copy link
Member

Choose a reason for hiding this comment

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

emmm, but that make BorrowedReference embarrassed.

Copy link
Member Author

Choose a reason for hiding this comment

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

@amos402 The idea with PyHandle can be reviewed separately. This PR is only for tracking new vs borrowed references as used in Python documentation for C API.

public IntPtr Pointer { get; set; }
public bool IsNull => this.Pointer == IntPtr.Zero;

public PyObject ToPyObject()
Copy link
Member

Choose a reason for hiding this comment

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

I prefer two methods:
ToPyObject: inref and create a PyObject, not assign the Pointer to nullptr
AsPyObject: use this implementation

Copy link
Member Author

Choose a reason for hiding this comment

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

Constructing a new PyObject from references should be done using PyObject constructors (which can be added later). I agree though, that ToPyObject is not descriptive enough. Maybe MoveToPyObject or IntoPyObject?

}

[Obsolete("Use overloads, that take BorrowedReference or NewReference")]
public IntPtr DangerousGetAddress()
Copy link
Member

Choose a reason for hiding this comment

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

Use a property named Address can be more fit?
Why I have to be warned if I trying use the raw pointer, seems it doesn't make any sense. Or just don't expose the interface and use explicit operator IntPtr instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is modeled after SafeHandle class and its DangerousGetHandle method.
The whole point of introducing these references is to get rid of IntPtrs entirely. It should warn you against converting into IntPtr. Maybe Obsolete part is unnecessary as Dangerous sounds like a warning enough. I will remove Obsolete.

removed public property Pointer from NewReference and replaced with DangerousGetAddress
@lostmsu
Copy link
Member Author

lostmsu commented Feb 26, 2020

@amos402 @filmor I urge you to review this ASAP. As I am investigating segfaults in #1050 and #1060 , I'd love to have type checker on my side. Will definitely bring this up during the next call.

BTW, I don't think it is necessary to update branch with the latest master every time before merging, as long as there are no conflicts. In an unlikely case, when it breaks the build, the change can be reverted and the PR reopened.

@filmor
Copy link
Member

filmor commented Feb 26, 2020

I like this a lot, I'm was just a bit reluctant about using C# 8 as it won't be officially supported on .NET Framework, but apparently you are not doing that yet.

@filmor filmor merged commit 770fc01 into pythonnet:master Feb 26, 2020
@amos402 amos402 mentioned this pull request Mar 10, 2020
4 tasks
AlexCatarino pushed a commit to QuantConnect/pythonnet that referenced this pull request Jun 29, 2020
* NewReference type and an example usage
* BorrowedReference + example, that exposes dangerous pattern
* make BorrowedReference readonly ref struct
* BorrowedReference.Pointer is a private readonly field
* renamed NewReference.ToPyObject to MoveToPyObject
* removed public property Pointer from NewReference and replaced with DangerousGetAddress
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants