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
Import hook as a module, take 2
  • Loading branch information
BadSingleton committed Jun 2, 2021
commit e71a0ef90414bd2cde21901ed3956251fccc4753
93 changes: 69 additions & 24 deletions src/runtime/importhook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import sys
class DotNetLoader(importlib.abc.Loader):

def __init__(self):
super(DotNetLoader, self).__init__()
super().__init__()
Comment thread
filmor marked this conversation as resolved.
Outdated

@classmethod
def exec_module(klass, mod):
Expand All @@ -29,21 +29,19 @@ def exec_module(klass, mod):
@classmethod
def create_module(klass, spec):
import clr
return clr._LoadClrModule(spec)
return clr._load_clr_module(spec)

class DotNetFinder(importlib.abc.MetaPathFinder):

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

@classmethod
def find_spec(klass, fullname, paths=None, target=None):
import clr
if (hasattr(clr, '_availableNamespaces') and fullname in clr._availableNamespaces):
if clr._availableNamespaces and fullname in clr._availableNamespaces:
return importlib.machinery.ModuleSpec(fullname, DotNetLoader(), is_package=True)
return None

sys.meta_path.append(DotNetFinder())
";
const string availableNsKey = "_availableNamespaces";

Expand All @@ -69,7 +67,7 @@ internal static unsafe void Initialize()

// Add/create the MetaPathLoader
SetupNamespaceTracking();
PythonEngine.Exec(LoaderCode);
SetupImportHook();
}


Expand Down Expand Up @@ -114,13 +112,66 @@ internal static void RestoreRuntimeData(RuntimeDataStorage storage)
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);

// 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);
Runtime.PyTuple_SetItem(args.DangerousGetAddress(), 0, codeStr);
// PyTuple_SetItem steals a reference, mod_dict is borrowed.
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)
{
Runtime.XDecref(import_hook_module);
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 metapath = Runtime.PySys_GetObject("meta_path");
Runtime.PyList_Append(metapath, finder);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These four lines look fishy: you don't use finder_inst and you are doing an incref on finder for no obvious reason.

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.

oh.. that should be finder_inst, not finder

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.

BTW, it makes sense to add an overload for PyList_Append with proper reference types, then avoid manual refcounting.

Same for PySys_GetObject - you can rename the original one to Dangerous_PySys_GetObject to avoid refactoring other call sites.

This is a NIT though. Should come up with a better plan to switch everything.

}

/// <summary>
/// Sets up the tracking of loaded namespaces. This makes available to
/// Python, as a Python object, the loaded namespaces. The set of loaded
/// namespaces is used during the import to verify if we can import a
/// CLR assembly as a module or not. The set is stored on the clr module.
/// </summary>
static void SetupNamespaceTracking ()
static void SetupNamespaceTracking()
{
var newset = Runtime.PySet_New(new BorrowedReference(IntPtr.Zero));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You Dispose in a finally; should this be a using var ?

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: I'd replace new BorrowedReference(IntPtr.Zero) with either BorrowedReference.Null or simply default.

try
Expand All @@ -130,7 +181,7 @@ static void SetupNamespaceTracking ()
var pyNs = Runtime.PyString_FromString(ns);
try
{
if(Runtime.PySet_Add(newset, new BorrowedReference(pyNs)) != 0)
if (Runtime.PySet_Add(newset, new BorrowedReference(pyNs)) != 0)
{
throw new PythonException();
}
Expand All @@ -141,7 +192,7 @@ static void SetupNamespaceTracking ()
}
}

if(Runtime.PyDict_SetItemString(root.dict, availableNsKey, newset.DangerousGetAddress()) != 0)
if (Runtime.PyDict_SetItemString(root.dict, availableNsKey, newset.DangerousGetAddress()) != 0)
{
throw new PythonException();
}
Expand All @@ -152,7 +203,7 @@ static void SetupNamespaceTracking ()
}

AssemblyManager.namespaceAdded += OnNamespaceAdded;
PythonEngine.AddShutdownHandler(()=>AssemblyManager.namespaceAdded -= OnNamespaceAdded);
PythonEngine.AddShutdownHandler(() => AssemblyManager.namespaceAdded -= OnNamespaceAdded);
}

/// <summary>
Expand All @@ -162,27 +213,21 @@ static void SetupNamespaceTracking ()
static void TeardownNameSpaceTracking()
{
AssemblyManager.namespaceAdded -= OnNamespaceAdded;
// If the C# runtime isn't loaded, then there is no namespaces available
if ((Runtime.PyDict_DelItemString(new BorrowedReference(root.dict), availableNsKey) != 0) &&
(Exceptions.ExceptionMatches(Exceptions.KeyError)))
{
// Trying to remove a key that's not in the dictionary
// raises an error. We don't care about it.
Runtime.PyErr_Clear();
}
// If the C# runtime isn't loaded, then there are no namespaces available
Runtime.PyDict_SetItemString(root.dict, availableNsKey, Runtime.PyNone);
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.

Why not remove the key entirely? You would not need to compare it to None below in that case.

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.

The DotNetFinder relies on that key being present on the object. If I remove it in AddNamespace, I need to add a check over there instead.

}

static void OnNamespaceAdded (string name)
static void OnNamespaceAdded(string name)
{
using(Py.GIL())
using (Py.GIL())
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.

Are there scenarios when this method will be called without GIL acquired?
If it is a module method, can it be attributed with allowthreads=false?

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.

It used to be possible. The method calling it now has the attribute

{
var pyNs = Runtime.PyString_FromString(name);
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 pyNs = NewReference.DangerousFromPoiner(Runtime.PyString_FromString(name)) would remove both try ... finally ... and a cast for PySet_Add

try
{
var nsSet = Runtime.PyDict_GetItemString(new BorrowedReference(root.dict), availableNsKey);
if (!nsSet.IsNull)
if (!nsSet.IsNull || nsSet.DangerousGetAddress() != Runtime.PyNone)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should BorrowedReference have an IsNullOrNone property? It seems likely to be a common idiom.

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 keep separate IsNull and IsNone. Thought there's already IsNone.

{
if(Runtime.PySet_Add(nsSet, new BorrowedReference(pyNs)) != 0)
if (Runtime.PySet_Add(nsSet, new BorrowedReference(pyNs)) != 0)
{
throw new PythonException();
}
Expand Down Expand Up @@ -225,7 +270,7 @@ public static unsafe NewReference GetCLRModule()
/// <summary>
/// The hook to import a CLR module into Python
/// </summary>
public static ModuleObject __import__(string modname)
public static ModuleObject Import(string modname)
{
// Traverse the qualified module name to get the named module.
// Note that if
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/moduleobject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,12 @@ 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 _LoadClrModule(PyObject spec)
public static PyObject _load_clr_module(PyObject spec)
{
ModuleObject mod = null;
using (var modname = spec.GetAttr("name"))
{
mod = ImportHook.__import__(modname.ToString());
mod = ImportHook.Import(modname.ToString());
}
// We can't return directly a ModuleObject, because the tpHandle is
// not set, but we can return a PyObject.
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/native/TypeOffset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ static void ValidateRequiredOffsetsPresent(PropertyInfo[] offsetProperties)
"getPreload",
"Initialize",
"ListAssemblies",
"_LoadClrModule",
"_load_clr_module",
"Release",
"Reset",
"set_SuppressDocs",
Expand Down