-
Notifications
You must be signed in to change notification settings - Fork 751
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
Proposal: Safe pointers #1043
Conversation
@@ -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(); |
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.
This generates a compiler warning, that uncovers a potentially dangerous pattern, where kwargsDict
may contain borrowed references.
src/runtime/pydict.cs
Outdated
@@ -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); |
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.
After dotnet/roslyn-analyzers#3305 is fixed we can configure build to fail here if using
would be omitted
@amos402 can you check this one out too? |
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
You can also implement an implicit operator for PyObject make it easy for usage. |
@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. |
@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 |
@@ -83,6 +83,7 @@ | |||
</Compile> | |||
<Compile Include="arrayobject.cs" /> | |||
<Compile Include="assemblymanager.cs" /> | |||
<Compile Include="BorrowedReference.cs" /> |
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.
Is it good for separate these three items to different files? They're small, related, and it make the project files be scattered.
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.
The might get more code later on. Generally, I prefer a type per file approach for anything except delegate types.
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.
Or create a folder for them? Just just too far if they sorted by name I thought🤣
{ | ||
using System; | ||
[NonCopyable] | ||
ref struct NewReference |
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.
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.
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.
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.
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.
But....sometimes I just want to put them into collections...😂
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.
Then you can still do DangerousGetHandle
or create a PyObject
from them.
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.
But this type also has a purpose for reminding others that it's a reference don't forget to decref it, isn't it?
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.
Because it must be clear from code, that care is necessary around this scenario.
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, 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.
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.
Maybe more introduce a type named PyHandle
for that?
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.
emmm, but that make BorrowedReference
embarrassed.
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.
@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.
src/runtime/NewReference.cs
Outdated
public IntPtr Pointer { get; set; } | ||
public bool IsNull => this.Pointer == IntPtr.Zero; | ||
|
||
public PyObject ToPyObject() |
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 prefer two methods:
ToPyObject
: inref and create a PyObject
, not assign the Pointer
to nullptr
AsPyObject
: use this implementation
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.
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() |
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.
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.
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.
This is modeled after SafeHandle
class and its DangerousGetHandle
method.
The whole point of introducing these references is to get rid of IntPtr
s 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
@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. |
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. |
* 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
What does this implement/fix? Explain your changes.
With C# 7.3
ref structs
and C# 8.0using
working on any method namedDispose
it is possible to track new vs borrowed references at compile time.This proposal introduces
BorrowedReference
andNewReference
types, that areref
-only structs, meaning you can't put them intoPyObject
directly, so they can implement differentToPyObject
methods, one doingIncRef
, and the other one not (alternatively, we can havePyObject
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 ofBorrowedReference
orNewReference
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 withtry ... finally ...
block.