using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using Python.Runtime.StateSerialization;
namespace Python.Runtime
{
///
/// Implements the "import hook" used to integrate Python with the CLR.
///
internal static class ImportHook
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
// set in Initialize
private static PyObject root;
private static CLRModule clrModule;
private static PyModule py_clr_module;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
internal static BorrowedReference ClrModuleReference => py_clr_module.Reference;
private const string LoaderCode = @"
import importlib.abc
import sys
class DotNetLoader(importlib.abc.Loader):
@classmethod
def exec_module(klass, mod):
# This method needs to exist.
pass
@classmethod
def create_module(klass, spec):
import clr
return clr._load_clr_module(spec)
class DotNetFinder(importlib.abc.MetaPathFinder):
@classmethod
def find_spec(klass, fullname, paths=None, target=None):
# Don't import, we might call ourselves recursively!
if 'clr' not in sys.modules:
return None
clr = sys.modules['clr']
clr._add_pending_namespaces()
if clr._available_namespaces and fullname in clr._available_namespaces:
return importlib.machinery.ModuleSpec(fullname, DotNetLoader(), is_package=True)
return None
";
const string _available_namespaces = "_available_namespaces";
///
/// Initialization performed on startup of the Python runtime.
///
internal static unsafe void Initialize()
{
// Initialize the clr module and tell Python about it.
root = CLRModule.Create(out clrModule).MoveToPyObject();
// create a python module with the same methods as the clr module-like object
py_clr_module = new PyModule(Runtime.PyModule_New("clr").StealOrThrow());
// both dicts are borrowed references
BorrowedReference mod_dict = Runtime.PyModule_GetDict(ClrModuleReference);
using var clr_dict = Runtime.PyObject_GenericGetDict(root);
Runtime.PyDict_Update(mod_dict, clr_dict.BorrowOrThrow());
BorrowedReference dict = Runtime.PyImport_GetModuleDict();
Runtime.PyDict_SetItemString(dict, "CLR", ClrModuleReference);
Runtime.PyDict_SetItemString(dict, "clr", ClrModuleReference);
SetupNamespaceTracking();
SetupImportHook();
}
///
/// Cleanup resources upon shutdown of the Python runtime.
///
internal static void Shutdown()
{
if (Runtime.Py_IsInitialized() == 0)
{
return;
}
TeardownNameSpaceTracking();
clrModule.ResetModuleMembers();
Runtime.Py_CLEAR(ref py_clr_module!);
root.Dispose();
root = null!;
}
private static Dictionary GetDotNetModules()
{
BorrowedReference pyModules = Runtime.PyImport_GetModuleDict();
using var items = Runtime.PyDict_Items(pyModules);
nint length = Runtime.PyList_Size(items.BorrowOrThrow());
Debug.Assert(length >= 0);
var modules = new Dictionary();
for (nint i = 0; i < length; i++)
{
BorrowedReference item = Runtime.PyList_GetItem(items.Borrow(), i);
BorrowedReference name = Runtime.PyTuple_GetItem(item, 0);
BorrowedReference module = Runtime.PyTuple_GetItem(item, 1);
if (ManagedType.IsInstanceOfManagedType(module))
{
modules.Add(new PyString(name), new PyObject(module));
}
}
return modules;
}
internal static ImportHookState SaveRuntimeData()
{
return new()
{
PyCLRModule = py_clr_module,
Root = new PyObject(root),
Modules = GetDotNetModules(),
};
}
private static void RestoreDotNetModules(Dictionary modules)
{
var pyMoudles = Runtime.PyImport_GetModuleDict();
foreach (var item in modules)
{
var moduleName = item.Key;
var module = item.Value;
int res = Runtime.PyDict_SetItem(pyMoudles, moduleName, module);
PythonException.ThrowIfIsNotZero(res);
item.Key.Dispose();
item.Value.Dispose();
}
modules.Clear();
}
internal static void RestoreRuntimeData(ImportHookState storage)
{
py_clr_module = storage.PyCLRModule;
var rootHandle = storage.Root;
root = new PyObject(rootHandle);
clrModule = (CLRModule)ManagedType.GetManagedObject(rootHandle)!;
BorrowedReference dict = Runtime.PyImport_GetModuleDict();
Runtime.PyDict_SetItemString(dict, "clr", ClrModuleReference);
SetupNamespaceTracking();
RestoreDotNetModules(storage.Modules);
}
static void SetupImportHook()
{
// Create the import hook module
using var import_hook_module = Runtime.PyModule_New("clr.loader");
BorrowedReference mod_dict = Runtime.PyModule_GetDict(import_hook_module.BorrowOrThrow());
Debug.Assert(mod_dict != null);
// Run the python code to create the module's classes.
var builtins = Runtime.PyEval_GetBuiltins();
var exec = Runtime.PyDict_GetItemString(builtins, "exec");
using var args = Runtime.PyTuple_New(2);
PythonException.ThrowIfIsNull(args);
using var codeStr = Runtime.PyString_FromString(LoaderCode);
Runtime.PyTuple_SetItem(args.Borrow(), 0, codeStr.StealOrThrow());
// reference not stolen due to overload incref'ing for us.
Runtime.PyTuple_SetItem(args.Borrow(), 1, mod_dict);
Runtime.PyObject_Call(exec, args.Borrow(), default).Dispose();
// Set as a sub-module of clr.
if(Runtime.PyModule_AddObject(ClrModuleReference, "loader", import_hook_module.Steal()) != 0)
{
throw PythonException.ThrowLastAsClrException();
}
// Finally, add the hook to the meta path
var findercls = Runtime.PyDict_GetItemString(mod_dict, "DotNetFinder");
using var finderCtorArgs = Runtime.PyTuple_New(0);
using var finder_inst = Runtime.PyObject_CallObject(findercls, finderCtorArgs.Borrow());
var metapath = Runtime.PySys_GetObject("meta_path");
PythonException.ThrowIfIsNotZero(Runtime.PyList_Append(metapath, finder_inst.BorrowOrThrow()));
}
///
/// 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.
///
static void SetupNamespaceTracking()
{
using var newset = Runtime.PySet_New(default);
foreach (var ns in AssemblyManager.GetNamespaces())
{
using var pyNs = Runtime.PyString_FromString(ns);
if (Runtime.PySet_Add(newset.Borrow(), pyNs.BorrowOrThrow()) != 0)
{
throw PythonException.ThrowLastAsClrException();
}
}
if (Runtime.PyDict_SetItemString(clrModule.dict, _available_namespaces, newset.Borrow()) != 0)
{
throw PythonException.ThrowLastAsClrException();
}
}
///
/// Removes the set of available namespaces from the clr module.
///
static void TeardownNameSpaceTracking()
{
// If the C# runtime isn't loaded, then there are no namespaces available
Runtime.PyDict_SetItemString(clrModule.dict, _available_namespaces, Runtime.PyNone);
}
static readonly ConcurrentQueue addPending = new();
public static void AddNamespace(string name) => addPending.Enqueue(name);
internal static int AddPendingNamespaces()
{
int added = 0;
while (addPending.TryDequeue(out string ns))
{
AddNamespaceWithGIL(ns);
added++;
}
return added;
}
internal static void AddNamespaceWithGIL(string name)
{
using var pyNs = Runtime.PyString_FromString(name);
var nsSet = Runtime.PyDict_GetItemString(clrModule.dict, _available_namespaces);
if (!(nsSet.IsNull || nsSet == Runtime.PyNone))
{
if (Runtime.PySet_Add(nsSet, pyNs.BorrowOrThrow()) != 0)
{
throw PythonException.ThrowLastAsClrException();
}
}
}
///
/// Because we use a proxy module for the clr module, we somtimes need
/// to force the py_clr_module to sync with the actual clr module's dict.
///
internal static void UpdateCLRModuleDict()
{
clrModule.InitializePreload();
// update the module dictionary with the contents of the root dictionary
clrModule.LoadNames();
BorrowedReference py_mod_dict = Runtime.PyModule_GetDict(ClrModuleReference);
using var clr_dict = Runtime.PyObject_GenericGetDict(root);
Runtime.PyDict_Update(py_mod_dict, clr_dict.BorrowOrThrow());
}
///
/// Return the clr python module (new reference)
///
public static unsafe NewReference GetCLRModule()
{
UpdateCLRModuleDict();
return new NewReference(py_clr_module);
}
///
/// The hook to import a CLR module into Python. Returns a new reference
/// to the module.
///
public static PyObject Import(string modname)
{
// Traverse the qualified module name to get the named module.
// Note that if
// we are running in interactive mode we pre-load the names in
// each module, which is often useful for introspection. If we
// are not interactive, we stick to just-in-time creation of
// objects at lookup time, which is much more efficient.
// NEW: The clr got a new module variable preload. You can
// enable preloading in a non-interactive python processing by
// setting clr.preload = True
ModuleObject? head = null;
ModuleObject tail = clrModule;
clrModule.InitializePreload();
string[] names = modname.Split('.');
foreach (string name in names)
{
using var nested = tail.GetAttribute(name, true);
if (nested.IsNull() || ManagedType.GetManagedObject(nested.Borrow()) is not ModuleObject module)
{
Exceptions.SetError(Exceptions.ImportError, $"'{name}' Is not a ModuleObject.");
throw PythonException.ThrowLastAsClrException();
}
if (head == null)
{
head = module;
}
tail = module;
if (CLRModule.preload)
{
tail.LoadNames();
}
}
return tail.Alloc().MoveToPyObject();
}
}
}