Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a321daa
(WIP) modernize the import hook
BadSingleton Jan 15, 2021
279b535
Add the loaded namespaces tracking
BadSingleton Jan 15, 2021
f92e95b
cleanup and changelog entry
BadSingleton Jan 15, 2021
afffc18
Fix a bug where clr wasn't in sys.modules after reload
BadSingleton Jan 18, 2021
d821c0f
Further refinements to setattr logic on ModuleObjects
BadSingleton Jan 19, 2021
e469a8a
fixups, add docs
BadSingleton Jan 20, 2021
685b972
merge fixup
BadSingleton Mar 4, 2021
be81364
(WIP) import hook in the pytohn module
BadSingleton Mar 10, 2021
73958ed
Revert "(WIP) import hook in the pytohn module"
BadSingleton Apr 12, 2021
e71a0ef
Import hook as a module, take 2
BadSingleton Apr 12, 2021
2af066d
fixup! Merge remote-tracking branch 'origin/master' into modernize-im…
BadSingleton Apr 12, 2021
bb490bf
fixup! fixup! Merge remote-tracking branch 'origin/master' into moder…
BadSingleton Apr 12, 2021
31ea876
Create a clr.loader module
BadSingleton Jun 1, 2021
c02d5c6
Test to test if the test passes
BadSingleton Jun 2, 2021
970a189
fix new exception usage
BadSingleton Jun 2, 2021
059605b
kick the build because I can't repro
BadSingleton Jun 2, 2021
ff170e9
Add Namespaces to the import hook only through AddReference
BadSingleton Jun 2, 2021
63de923
Review changes, update API usage
BadSingleton Jun 8, 2021
624f7e3
Merge remote-tracking branch 'upstream/master' into modernize-import-…
BadSingleton Jun 14, 2021
bd7e745
make PyModule_AddObject in line with CPython
BadSingleton Jun 14, 2021
46a85fe
take care of stragglers
BadSingleton Jun 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Create a clr.loader module
* Return ModuleObject.pyHandle, do not convert.
* Write domain tests generated code to file.
  • Loading branch information
BadSingleton committed Jun 2, 2021
commit 31ea87698811ac382243f7328bc0885bfb4e5c75
8 changes: 8 additions & 0 deletions src/runtime/converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ internal static IntPtr ToPython(object value, Type type)
return ClassDerivedObject.ToPython(pyderived);
}

// ModuleObjects are created in a way that their wrapping them as
// a CLRObject fails, the ClassObject has no tpHandle. Return the
// pyHandle as is, do not convert.
if (value is ModuleObject modobj)
{
return modobj.pyHandle;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You need to return a new reference here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@BadSingleton what about this one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My bad missed that one.

}

// hmm - from Python, we almost never care what the declared
// type is. we'd rather have the object bound to the actual
// implementing class.
Expand Down
65 changes: 20 additions & 45 deletions src/runtime/importhook.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace Python.Runtime
Expand All @@ -18,9 +19,6 @@ import sys

class DotNetLoader(importlib.abc.Loader):

def __init__(self):
super().__init__()

@classmethod
def exec_module(klass, mod):
# This method needs to exist.
Expand All @@ -32,13 +30,13 @@ import clr
return clr._load_clr_module(spec)

class DotNetFinder(importlib.abc.MetaPathFinder):

def __init__(self):
super().__init__()


@classmethod
def find_spec(klass, fullname, paths=None, target=None):
import clr
# Don't import, we might call ourselves recursively!
if 'clr' not in sys.modules:
return None
clr = sys.modules['clr']
if clr._available_namespaces and fullname in clr._available_namespaces:
return importlib.machinery.ModuleSpec(fullname, DotNetLoader(), is_package=True)
return None
Expand All @@ -64,13 +62,10 @@ internal static unsafe void Initialize()
BorrowedReference dict = Runtime.PyImport_GetModuleDict();
Runtime.PyDict_SetItemString(dict, "CLR", ClrModuleReference);
Runtime.PyDict_SetItemString(dict, "clr", ClrModuleReference);

// Add/create the MetaPathLoader
SetupNamespaceTracking();
SetupImportHook();
}


/// <summary>
/// Cleanup resources upon shutdown of the Python runtime.
/// </summary>
Expand All @@ -81,11 +76,10 @@ internal static void Shutdown()
return;
}

bool shouldFreeDef = Runtime.Refcount(py_clr_module) == 1;
TeardownNameSpaceTracking();
Runtime.XDecref(py_clr_module);
py_clr_module = IntPtr.Zero;

TeardownNameSpaceTracking();
Runtime.XDecref(root.pyHandle);
root = null;
CLRModule.Reset();
Expand All @@ -107,62 +101,42 @@ internal static void RestoreRuntimeData(RuntimeDataStorage storage)
var rootHandle = storage.GetValue<IntPtr>("root");
root = (CLRModule)ManagedType.GetManagedObject(rootHandle);
BorrowedReference dict = Runtime.PyImport_GetModuleDict();
Runtime.PyDict_SetItemString(dict.DangerousGetAddress(), "CLR", py_clr_module);
Runtime.PyDict_SetItemString(dict.DangerousGetAddress(), "clr", py_clr_module);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd add a property ClrModuleReference that returns a BorrowedReference of py_clr_module, and then used it in PyDict_SetItemString overload, that works with BorrowedReferences to avoid using Dangerous* methods.

SetupNamespaceTracking();
}

static void SetupImportHook()
{
// Create the import hook module
var import_hook_module_def = ModuleDefOffset.AllocModuleDef("clr.loader");
var import_hook_module = Runtime.PyModule_Create2(import_hook_module_def, 3);
var import_hook_module = Runtime.PyModule_New("clr.loader");

// Run the python code to create the module's classes.
var mod_dict = Runtime.PyModule_GetDict(new BorrowedReference(import_hook_module));
var builtins = Runtime.PyEval_GetBuiltins();
var exec = Runtime.PyDict_GetItemString(builtins, "exec");
using var args = NewReference.DangerousFromPointer(Runtime.PyTuple_New(2));

var codeStr = Runtime.PyString_FromString(LoaderCode);
IntPtr codeStr = Runtime.PyString_FromString(LoaderCode);
Runtime.PyTuple_SetItem(args.DangerousGetAddress(), 0, codeStr);
// PyTuple_SetItem steals a reference, mod_dict is borrowed.
var mod_dict = Runtime.PyModule_GetDict(import_hook_module);
Runtime.XIncref(mod_dict.DangerousGetAddress());
Runtime.PyTuple_SetItem(args.DangerousGetAddress(), 1, mod_dict.DangerousGetAddress());
Runtime.PyObject_Call(exec.DangerousGetAddress(), args.DangerousGetAddress(), IntPtr.Zero);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is better to make an overload with the new reference types when possible without rewriting existing code, than to call Dangerous* methods. See for example

internal static IntPtr PyObject_Type(IntPtr op)

One can also be made for PyTuple_SetItem using the new StolenReference.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This applies to a few Runtime methods across the PR. Basically anything where you can add a new overload.


var loader = Runtime.PyDict_GetItemString(mod_dict, "DotNetLoader").DangerousGetAddressOrNull();
Runtime.XIncref(loader);

// Add the classes to the module
// PyModule_AddObject steals a reference only on success
if (Runtime.PyModule_AddObject(import_hook_module, "DotNetLoader", loader) != 0)
{
Runtime.XDecref(loader);
throw new PythonException();
}

var finder = Runtime.PyDict_GetItemString(mod_dict, "DotNetFinder").DangerousGetAddressOrNull();
Runtime.XIncref(finder);
if (Runtime.PyModule_AddObject(import_hook_module, "DotNetFinder", finder) != 0)
{
Runtime.XDecref(finder);
throw new PythonException();
}

// Set as a sub-module of clr.
Runtime.XIncref(import_hook_module);
if(Runtime.PyModule_AddObject(py_clr_module, "loader", import_hook_module) != 0)
if(Runtime.PyModule_AddObject(ClrModuleReference, "loader", import_hook_module) != 0)
{
Runtime.XDecref(import_hook_module);
Runtime.XDecref(import_hook_module.DangerousGetAddress());
Comment thread
lostmsu marked this conversation as resolved.
throw new PythonException();
}

// Finally, add the hook to the meta path
var finder_inst = Runtime.PyDict_GetItemString(mod_dict, "finder_inst").DangerousGetAddressOrNull();
Runtime.XIncref(finder);
var findercls = Runtime.PyDict_GetItemString(mod_dict, "DotNetFinder");
var finderCtorArgs = Runtime.PyTuple_New(0);
var finder_inst = Runtime.PyObject_CallObject(findercls.DangerousGetAddress(), finderCtorArgs);
Runtime.XDecref(finderCtorArgs);
var metapath = Runtime.PySys_GetObject("meta_path");
Runtime.PyList_Append(metapath, finder);
Runtime.PyList_Append(metapath, finder_inst);
}

/// <summary>
Expand Down Expand Up @@ -268,7 +242,8 @@ public static unsafe NewReference GetCLRModule()
}

/// <summary>
/// The hook to import a CLR module into Python
/// The hook to import a CLR module into Python. Returns a new reference
/// to the module.
/// </summary>
public static ModuleObject Import(string modname)
{
Expand Down Expand Up @@ -305,7 +280,7 @@ public static ModuleObject Import(string modname)
tail.LoadNames();
}
}

tail.IncrRefCount();
return tail;
}

Expand Down
15 changes: 3 additions & 12 deletions src/runtime/moduleobject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public ModuleObject(string name)
var dictRef = Runtime.PyObject_GenericGetDict(ObjectReference);
PythonException.ThrowIfIsNull(dictRef);
dict = dictRef.DangerousMoveToPointer();

__all__ = Runtime.PyList_New(0);
using var pyname = NewReference.DangerousFromPointer(Runtime.PyString_FromString(moduleName));
using var pyfilename = NewReference.DangerousFromPointer(Runtime.PyString_FromString(filename));
using var pydocstring = NewReference.DangerousFromPointer(Runtime.PyString_FromString(docstring));
Expand Down Expand Up @@ -576,13 +576,6 @@ public static string[] ListAssemblies(bool verbose)
return names;
}

[ModuleFunction]
public static int _AtExit()
{
return Runtime.AtExit();
}


/// <summary>
/// Note: This should *not* be called directly.
/// The function that get/import a CLR assembly as a python module.
Expand All @@ -593,16 +586,14 @@ public static int _AtExit()
/// <returns>A new reference to the imported module, as a PyObject.</returns>
[ModuleFunction]
[ForbidPythonThreads]
Comment thread
lostmsu marked this conversation as resolved.
public static PyObject _load_clr_module(PyObject spec)
public static ModuleObject _load_clr_module(PyObject spec)
{
ModuleObject mod = null;
using (var modname = spec.GetAttr("name"))
{
mod = ImportHook.Import(modname.ToString());
}
// We can't return directly a ModuleObject, because the tpHandle is
// not set, but we can return a PyObject.
return new PyObject(mod.pyHandle);
return mod;
}
Comment on lines +600 to +604
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NIT:

using var  modname = ...;
return ...;

}
}
6 changes: 3 additions & 3 deletions src/runtime/runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1944,7 +1944,7 @@ internal static string PyModule_GetFilename(IntPtr module)


internal static IntPtr PyImport_Import(IntPtr name) => Delegates.PyImport_Import(name);
internal static int PyModule_AddObject(IntPtr module, string name, IntPtr stolenObject)
internal static int PyModule_AddObject(BorrowedReference module, string name, BorrowedReference stolenObject)
Copy link
Copy Markdown
Member

@lostmsu lostmsu Jun 3, 2021

Choose a reason for hiding this comment

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

Looks like stolenObject parameter should be of type StolenReference here and below (present in the latest master).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

StolenReference here and below (present in the latest master)

🎉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Although I'm reviewing the semantics, and this function only steals on success, so by using StolenReference, we leak on failure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dang you are right. Why did they not make it consistent? PyTuple_SetItem for example does steal even when there's an error. ugh...

Maybe we should make a wrapper, that always steals?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that we should not use BorrowedReference for the last parameter here. It makes false suggestion, that signature is valid, and borrowed reference is always enough when it is really not. I'd keep it IntPtr for now, and explain the behavior in the parameter doc.

Or alternatively, make a wrapper, that takes either BorrowedReference or StolenReference and handles refcounting internally, and keep the real PInvoke private.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm leaning towards the IntPtr parameter to be as "consistant" as possible with the CPython API.

{
using var namePtr = new StrPtr(name, Encoding.UTF8);
return Delegates.PyModule_AddObject(module, namePtr, stolenObject);
Expand Down Expand Up @@ -2507,7 +2507,7 @@ static Delegates()
{
PyModule_Create2 = (delegate* unmanaged[Cdecl]<IntPtr, int, IntPtr>)GetFunctionByName("PyModule_Create2TraceRefs", GetUnmanagedDll(_PythonDll));
}
PyModule_AddObject = (delegate* unmanaged[Cdecl]<IntPtr, StrPtr, IntPtr, int>)GetFunctionByName(nameof(PyModule_AddObject), GetUnmanagedDll(_PythonDll));
PyModule_AddObject = (delegate* unmanaged[Cdecl]<BorrowedReference, StrPtr, BorrowedReference, int>)GetFunctionByName(nameof(PyModule_AddObject), GetUnmanagedDll(_PythonDll));
PyImport_Import = (delegate* unmanaged[Cdecl]<IntPtr, IntPtr>)GetFunctionByName(nameof(PyImport_Import), GetUnmanagedDll(_PythonDll));
PyImport_ImportModule = (delegate* unmanaged[Cdecl]<StrPtr, NewReference>)GetFunctionByName(nameof(PyImport_ImportModule), GetUnmanagedDll(_PythonDll));
PyImport_ReloadModule = (delegate* unmanaged[Cdecl]<BorrowedReference, NewReference>)GetFunctionByName(nameof(PyImport_ReloadModule), GetUnmanagedDll(_PythonDll));
Expand Down Expand Up @@ -2797,7 +2797,7 @@ static Delegates()
internal static delegate* unmanaged[Cdecl]<BorrowedReference, BorrowedReference> PyModule_GetDict { get; }
internal static delegate* unmanaged[Cdecl]<IntPtr, StrPtr> PyModule_GetFilename { get; }
internal static delegate* unmanaged[Cdecl]<IntPtr, int, IntPtr> PyModule_Create2 { get; }
internal static delegate* unmanaged[Cdecl]<IntPtr, StrPtr, IntPtr, int> PyModule_AddObject { get; }
internal static delegate* unmanaged[Cdecl]<BorrowedReference, StrPtr, BorrowedReference, int> PyModule_AddObject { get; }
internal static delegate* unmanaged[Cdecl]<IntPtr, IntPtr> PyImport_Import { get; }
internal static delegate* unmanaged[Cdecl]<StrPtr, NewReference> PyImport_ImportModule { get; }
internal static delegate* unmanaged[Cdecl]<BorrowedReference, NewReference> PyImport_ReloadModule { get; }
Expand Down
13 changes: 12 additions & 1 deletion tests/domain_tests/TestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ namespace Python.DomainReloadTests
/// which test case to run. That's because pytest assumes we'll run
/// everything in one process, but we really want a clean process on each
/// test case to test the init/reload/teardown parts of the domain reload.
///
/// ### Debugging tips: ###
/// * Running pytest with the `-s` argument prevents stdout capture by pytest
/// * Add a sleep into the python test case before the crash/failure, then while
/// sleeping, attach the debugger to the Python.TestDomainReload.exe process.
/// </summary>
///
class TestRunner
Expand Down Expand Up @@ -1287,7 +1292,13 @@ static string CreateAssembly(string name, string code, bool exe = false)
}
parameters.ReferencedAssemblies.Add(netstandard);
parameters.ReferencedAssemblies.Add(PythonDllLocation);
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
// Write code to file so it can debugged.
var sourcePath = Path.Combine(TestPath, name+"_source.cs");
using(var file = new StreamWriter(sourcePath))
{
file.Write(code);
}
CompilerResults results = provider.CompileAssemblyFromFile(parameters, sourcePath);
if (results.NativeCompilerReturnValue != 0)
{
var stderr = System.Console.Error;
Expand Down
1 change: 0 additions & 1 deletion tests/domain_tests/test_domain_reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,5 @@ def test_in_to_ref_param():
def test_nested_type():
_run_test("nested_type")

@pytest.mark.skipif(platform.system() == 'Darwin', reason='FIXME: macos can\'t find the python library')
def test_import_after_reload():
_run_test("import_after_reload")