From 9b5914e3cfcd57bf4f5d4afc8d40c4733aedc4aa Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:14:08 +0100 Subject: [PATCH 1/8] Workaround for setuptools bug #4759 --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 94857e4..c3ed5df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61", "setuptools_scm[toml]", "wheel"] +requires = ["setuptools>=75", "setuptools_scm[toml]"] build-backend = "setuptools.build_meta" [project] @@ -43,6 +43,7 @@ dev = [ [tool.setuptools] zip-safe = false package-data = {"clr_loader.ffi" = ["dlls/x86/*.dll", "dlls/amd64/*.dll"]} +license-files = [] [tool.setuptools.packages.find] include = ["clr_loader*"] From cbe765c35ecf8c1885be4f2eef4e9aea7d8ce406 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:33:46 +0100 Subject: [PATCH 2/8] Use dependency group instead of optional deps --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c3ed5df..2c643e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ email = "filmor@gmail.com" Sources = "https://github.com/pythonnet/clr-loader" Documentation = "https://pythonnet.github.io/clr-loader/" -[optional-dependencies] +[dependency-groups] dev = [ "pytest" ] From 80e2385f5e1ba3ed2292948422514878b951793f Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 24 Jul 2025 19:55:34 +0200 Subject: [PATCH 3/8] Update CI actions (#79) * Update CI actions * Fix format --- .github/workflows/ci-arm.yml | 5 ++--- .github/workflows/ci.yml | 22 +++++++++++----------- .github/workflows/docs.yml | 2 +- clr_loader/types.py | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml index c451f0f..8f46757 100644 --- a/.github/workflows/ci-arm.yml +++ b/.github/workflows/ci-arm.yml @@ -2,7 +2,7 @@ name: ARM64 Tests on: push: - branches: master + branches: [master] pull_request: jobs: @@ -10,13 +10,12 @@ jobs: runs-on: [self-hosted, linux, ARM64] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x 6.0.x - name: Create virtualenv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae40be9..b83433d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,22 +2,22 @@ name: Python Tests on: push: - branches: master + branches: [master] pull_request: jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-dotnet@v1 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v6 - name: Build run: uv build - name: Upload source distribution - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-output path: "dist/*" @@ -26,10 +26,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depths: 0 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v6 - name: Install Ruff run: uv tool install ruff - name: Check format @@ -46,7 +46,7 @@ jobs: python: ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8'] # pypy3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v1 @@ -54,13 +54,13 @@ jobs: dotnet-version: '6.0.x' - name: Set up Python ${{ matrix.python }} - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python }} - name: Cache Mono if: runner.os == 'Windows' - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ env.TEMP }}\chocolatey key: ${{ runner.os }}-chocolatey-${{ matrix.python == 'pypy3' && '32' || '64' }} @@ -76,7 +76,7 @@ jobs: uv pip install pytest - name: Download wheel - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-output path: dist/ @@ -96,7 +96,7 @@ jobs: steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-output path: dist/ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bce19d4..1fe8fee 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Upload artifact # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: doc/_build/html/ diff --git a/clr_loader/types.py b/clr_loader/types.py index 15c1e30..6b54030 100644 --- a/clr_loader/types.py +++ b/clr_loader/types.py @@ -141,6 +141,6 @@ def _truncate(string: str, length: int) -> str: if length <= 1: raise TypeError("length must be > 1") if len(string) > length - 1: - return f"{string[:length-1]}…" + return f"{string[: length - 1]}…" else: return string From f9e0544ff02c6fced35e47a3593d477dd17a4cb2 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Mon, 28 Jul 2025 16:20:14 -0500 Subject: [PATCH 4/8] add trace options (#67) Co-authored-by: Mohamed Koubaa --- clr_loader/__init__.py | 2 ++ clr_loader/ffi/mono.py | 3 +++ clr_loader/mono.py | 12 ++++++++++++ tests/test_common.py | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 71eb09d..52ce603 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -33,6 +33,8 @@ def get_mono( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ) -> Runtime: """Get a Mono runtime instance diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index c194393..a3249a1 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -44,5 +44,8 @@ void mono_set_signal_chaining(bool chain_signals); +void mono_trace_set_level_string(const char* value); +void mono_trace_set_mask_string(const char* value); + """ ) diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 1899ea3..f7f90e5 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -27,6 +27,8 @@ def __init__( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ): self._assemblies: Dict[Path, Any] = {} @@ -39,6 +41,8 @@ def __init__( assembly_dir=assembly_dir, config_dir=config_dir, set_signal_chaining=set_signal_chaining, + trace_mask=trace_mask, + trace_level=trace_level, ) if domain is None: @@ -131,11 +135,19 @@ def initialize( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ) -> str: global _MONO, _ROOT_DOMAIN if _MONO is None: _MONO = load_mono(libmono) + if trace_mask is not None: + _MONO.mono_trace_set_mask_string(trace_mask.encode("utf8")) + + if trace_level is not None: + _MONO.mono_trace_set_level_string(trace_level.encode("utf8")) + if assembly_dir is not None and config_dir is not None: _MONO.mono_set_dirs(assembly_dir.encode("utf8"), config_dir.encode("utf8")) diff --git a/tests/test_common.py b/tests/test_common.py index 139f192..3eaf215 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -56,6 +56,24 @@ def test_mono_signal_chaining(example_netstandard: Path): run_tests(asm) +def test_mono_trace_mask(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(trace_mask="all") + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +def test_mono_trace_level(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(trace_level="message") + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + def test_mono_set_dir(example_netstandard: Path): from clr_loader import get_mono From 5dadf4cedbe8e68075d50e626f26a8fc556591af Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Thu, 14 Aug 2025 16:44:40 -0500 Subject: [PATCH 5/8] Support pythonnet for AppDomain (#78) * load the assembly and get function address inside the domain When using domain.Load for an assembly, the assembly resolution rules are awkward Even if the full path is given to the AssemblyName, when the domain tries to load the assembly, it does not use that context and tries to resolve the assembly using normal domain resolution rules, which would require an assembly resolver to be installed. However, the assembly resolver that is actually used at runtime is the one installed to the main appdomain. This prevents a library like Python.Runtime.dll (used by pythonnet) which is not installed to the application base directory to be loaded by clr_loader. To fix this issue, the assembly resolver of the main appdomain is lazily extending to include paths needed for libraries passed into GetFunction, and GetFunction internally uses AppDomain.DoCallBack() to marshal the function pointer inside the target app domain, using global domain data to access the function pointer and return it to the user of clr_loader. * Add comment * PR review feedback --------- Co-authored-by: Mohamed Koubaa --- netfx_loader/ClrLoader.cs | 25 ++++++++-- netfx_loader/DomainData.cs | 99 ++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 24 deletions(-) diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index 32b4c01..2a065bb 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Runtime.InteropServices; using NXPorts.Attributes; @@ -21,6 +22,19 @@ public static void Initialize() } } + private static string AssemblyDirectory + { + get + { + // This is needed in case the DLL was shadow-copied + // (Otherwise .Location would work) + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + UriBuilder uri = new UriBuilder(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + [DllExport("pyclr_create_appdomain", CallingConvention.Cdecl)] public static IntPtr CreateAppDomain( [MarshalAs(UnmanagedType.LPUTF8Str)] string name, @@ -28,16 +42,17 @@ public static IntPtr CreateAppDomain( ) { Print($"Creating AppDomain {name} with {configFile}"); + + var clrLoaderDir = AssemblyDirectory; if (!string.IsNullOrEmpty(name)) { var setup = new AppDomainSetup { - ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, + ApplicationBase = clrLoaderDir, ConfigurationFile = configFile }; - Print($"Base: {AppDomain.CurrentDomain.BaseDirectory}"); + Print($"Base: {clrLoaderDir}"); var domain = AppDomain.CreateDomain(name, null, setup); - Print($"Located domain {domain}"); var domainData = new DomainData(domain); @@ -61,8 +76,8 @@ public static IntPtr GetFunction( try { var domainData = _domains[(int)domain]; - var deleg = domainData.GetEntryPoint(assemblyPath, typeName, function); - return Marshal.GetFunctionPointerForDelegate(deleg); + Print($"Getting functor for function {function} of type {typeName} in assembly {assemblyPath}"); + return domainData.GetFunctor(assemblyPath, typeName, function); } catch (Exception exc) { diff --git a/netfx_loader/DomainData.cs b/netfx_loader/DomainData.cs index 3a17d7a..35c3363 100644 --- a/netfx_loader/DomainData.cs +++ b/netfx_loader/DomainData.cs @@ -1,54 +1,115 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; namespace ClrLoader { using static ClrLoader; - class DomainData : IDisposable + public static class DomainSetup { public delegate int EntryPoint(IntPtr buffer, int size); + public static void StoreFunctorFromDomainData() + { + var domain = AppDomain.CurrentDomain; + var assemblyPath = (string)domain.GetData("_assemblyPath"); + var typeName = (string)domain.GetData("_typeName"); + var function = (string)domain.GetData("_function"); + var deleg = GetDelegate(domain, assemblyPath, typeName, function); + var functor = Marshal.GetFunctionPointerForDelegate(deleg); + domain.SetData("_thisDelegate", deleg); + domain.SetData("_thisFunctor", functor); + } + + private static Delegate GetDelegate(AppDomain domain, string assemblyPath, string typeName, string function) + { + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath); + var assembly = domain.Load(assemblyName); + var type = assembly.GetType(typeName, throwOnError: true); + var deleg = Delegate.CreateDelegate(typeof(EntryPoint), type, function); + return deleg; + } + } + + class DomainData : IDisposable + { bool _disposed = false; public AppDomain Domain { get; } - public Dictionary<(string, string, string), EntryPoint> _delegates; + public Dictionary<(string, string, string), IntPtr> _functors; + public HashSet _resolvedAssemblies; public DomainData(AppDomain domain) { Domain = domain; - _delegates = new Dictionary<(string, string, string), EntryPoint>(); + _functors = new Dictionary<(string, string, string), IntPtr>(); + _resolvedAssemblies = new HashSet(); } - public EntryPoint GetEntryPoint(string assemblyPath, string typeName, string function) + private void installResolver(string assemblyPath) { - if (_disposed) - throw new InvalidOperationException("Domain is already disposed"); - - var key = (assemblyPath, typeName, function); - - EntryPoint result; + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + if (_resolvedAssemblies.Contains(assemblyName)) + return; + _resolvedAssemblies.Add(assemblyName); - if (!_delegates.TryGetValue(key, out result)) + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { - var assembly = Domain.Load(AssemblyName.GetAssemblyName(assemblyPath)); - var type = assembly.GetType(typeName, throwOnError: true); + if (args.Name.Contains(assemblyName)) + return Assembly.LoadFrom(assemblyPath); + return null; + }; + } - Print($"Loaded type {type}"); - result = (EntryPoint)Delegate.CreateDelegate(typeof(EntryPoint), type, function); + private static readonly object _lockObj = new object(); - _delegates[key] = result; - } + public IntPtr GetFunctor(string assemblyPath, string typeName, string function) + { + if (_disposed) + throw new InvalidOperationException("Domain is already disposed"); - return result; + // neither the domain data nor the _functors dictionary is threadsafe + lock (_lockObj) + { + installResolver(assemblyPath); + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + + var key = (assemblyName, typeName, function); + + IntPtr result; + if (!_functors.TryGetValue(key, out result)) + { + Domain.SetData("_assemblyPath", assemblyPath); + Domain.SetData("_typeName", typeName); + Domain.SetData("_function", function); + + Domain.DoCallBack(new CrossAppDomainDelegate(DomainSetup.StoreFunctorFromDomainData)); + result = (IntPtr)Domain.GetData("_thisFunctor"); + if (result == IntPtr.Zero) + throw new Exception($"Unable to get functor for {assemblyName}, {typeName}, {function}"); + + // set inputs to StoreFunctorFromDomainData to null. + // (There's no method to explicitly clear domain data) + Domain.SetData("_assemblyPath", null); + Domain.SetData("_typeName", null); + Domain.SetData("_function", null); + + // the result has to remain in the domain data because we don't know when the + // client of pyclr_get_function will actually invoke the functor, and if we + // remove it from the domain data after returning it may get collected too early. + _functors[key] = result; + } + return result; + } } public void Dispose() { if (!_disposed) { - _delegates.Clear(); + _functors.Clear(); if (Domain != AppDomain.CurrentDomain) AppDomain.Unload(Domain); From c3445ff0f5f7212154224717b29f64c48a81b88e Mon Sep 17 00:00:00 2001 From: Etienne Gaudrain Date: Thu, 14 Aug 2025 23:51:10 +0200 Subject: [PATCH 6/8] Added extra info about error when hostfxr not found or loaded properly. (#76) https://github.com/pythonnet/clr-loader/issues/75 --- clr_loader/ffi/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 23debae..289999d 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -23,18 +23,20 @@ def load_hostfxr(dotnet_root: Path): hostfxr_path = dotnet_root / "host" / "fxr" hostfxr_paths = hostfxr_path.glob(f"?.*/{hostfxr_name}") + error_report = list() + for hostfxr_path in reversed(sorted(hostfxr_paths, key=_path_to_version)): try: return ffi.dlopen(str(hostfxr_path)) - except Exception: - pass + except Exception as err: + error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") try: return ffi.dlopen(str(dotnet_root / hostfxr_name)) - except Exception: - pass + except Exception as err: + error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") - raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}") + raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n"+("\n\n".join(error_report))) def load_mono(path: Optional[Path] = None): From eeed6ec9ccc1be5222eb192beaef28570223a47e Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 14 Aug 2025 23:53:35 +0200 Subject: [PATCH 7/8] Fix format --- clr_loader/ffi/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 289999d..7917af9 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -36,7 +36,10 @@ def load_hostfxr(dotnet_root: Path): except Exception as err: error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") - raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n"+("\n\n".join(error_report))) + raise RuntimeError( + f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n" + + ("\n\n".join(error_report)) + ) def load_mono(path: Optional[Path] = None): From 8c123c6147b6b7094cd6a61031db47156bff5522 Mon Sep 17 00:00:00 2001 From: Steven Lovelock Date: Thu, 14 Aug 2025 23:01:30 +0100 Subject: [PATCH 8/8] Add get_coreclr_command_line which uses hostfxr_initialize_for_dotnet_command_line (#66) see https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#initializing-host-context --- clr_loader/__init__.py | 34 ++++++++++++++++++++++ clr_loader/hostfxr.py | 64 ++++++++++++++++++++++++++++++++++-------- tests/test_common.py | 13 +++++++++ 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 52ce603..2744cf0 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -11,6 +11,7 @@ "get_mono", "get_netfx", "get_coreclr", + "get_coreclr_command_line", "find_dotnet_root", "find_libmono", "find_runtimes", @@ -152,6 +153,39 @@ def get_coreclr( return impl +def get_coreclr_command_line( + *, + entry_dll: StrOrPath, + dotnet_root: Optional[StrOrPath] = None, + properties: Optional[Dict[str, str]] = None +) -> Runtime: + """Get a CoreCLR (.NET Core) runtime instance + The returned ``DotnetCoreRuntimeCommandLine`` also acts as a mapping of the config + properties. They can be retrieved using the index operator and can be + written until the runtime is initialized. The runtime is initialized when + the first function object is retrieved. + :param entry_dll: + The path to the entry dll. + :param dotnet_root: + The root directory of the .NET Core installation. If this is not + specified, we try to discover it using :py:func:`find_dotnet_root`. + :param properties: + Additional runtime properties. These can also be passed using the + ``configProperties`` section in the runtime config.""" + from .hostfxr import DotnetCoreCommandRuntime + + dotnet_root = _maybe_path(dotnet_root) + if dotnet_root is None: + dotnet_root = find_dotnet_root() + + impl = DotnetCoreCommandRuntime(entry_dll=_maybe_path(entry_dll), dotnet_root=dotnet_root) + if properties: + for key, value in properties.items(): + impl[key] = value + + return impl + + def get_netfx( *, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None ) -> Runtime: diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index 225b4c7..7a159f3 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -6,13 +6,15 @@ from .types import Runtime, RuntimeInfo, StrOrPath from .util import check_result -__all__ = ["DotnetCoreRuntime"] +__all__ = ["DotnetCoreRuntime", "DotnetCoreCommandRuntime"] _IS_SHUTDOWN = False -class DotnetCoreRuntime(Runtime): - def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): +class DotnetCoreRuntimeBase(Runtime): + _version: str + + def __init__(self, dotnet_root: Path): self._handle = None if _IS_SHUTDOWN: @@ -20,15 +22,8 @@ def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): self._dotnet_root = Path(dotnet_root) self._dll = load_hostfxr(self._dotnet_root) - self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config) self._load_func = None - for key, value in params.items(): - self[key] = value - - # TODO: Get version - self._version = "" - @property def dotnet_root(self) -> Path: return self._dotnet_root @@ -122,7 +117,31 @@ def info(self): ) -def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): +class DotnetCoreRuntime(DotnetCoreRuntimeBase): + def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): + super().__init__(dotnet_root) + self._handle = _get_handle_for_runtime_config(self._dll, self._dotnet_root, runtime_config) + + for key, value in params.items(): + self[key] = value + + # TODO: Get version + self._version = "" + + +class DotnetCoreCommandRuntime(DotnetCoreRuntimeBase): + def __init__(self, entry_dll: Path, dotnet_root: Path, **params: str): + super().__init__(dotnet_root) + self._handle = _get_handle_for_dotnet_command_line(self._dll, self._dotnet_root, entry_dll) + + for key, value in params.items(): + self[key] = value + + # TODO: Get version + self._version = "" + + +def _get_handle_for_runtime_config(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): params = ffi.new("hostfxr_initialize_parameters*") params.size = ffi.sizeof("hostfxr_initialize_parameters") # params.host_path = ffi.new("char_t[]", encode(sys.executable)) @@ -140,6 +159,29 @@ def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): return handle_ptr[0] +def _get_handle_for_dotnet_command_line(dll, dotnet_root: StrOrPath, entry_dll: StrOrPath): + params = ffi.new("hostfxr_initialize_parameters*") + params.size = ffi.sizeof("hostfxr_initialize_parameters") + params.host_path = ffi.NULL + dotnet_root_p = ffi.new("char_t[]", encode(str(Path(dotnet_root)))) + params.dotnet_root = dotnet_root_p + + handle_ptr = ffi.new("hostfxr_handle*") + + args_ptr = ffi.new("char_t*[1]") + arg_ptr = ffi.new("char_t[]", encode(str(Path(entry_dll)))) + args_ptr[0] = arg_ptr + res = dll.hostfxr_initialize_for_dotnet_command_line( + 1, + args_ptr, + params, handle_ptr + ) + + check_result(res) + + return handle_ptr[0] + + def _get_load_func(dll, handle): delegate_ptr = ffi.new("void**") diff --git a/tests/test_common.py b/tests/test_common.py index 3eaf215..b9b0597 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -92,6 +92,19 @@ def test_coreclr(example_netcore: Path): run_tests(asm) +def test_coreclr_command_line(example_netcore: Path): + run_in_subprocess(_do_test_coreclr_command_line, example_netcore) + + +def _do_test_coreclr_command_line(example_netcore): + from clr_loader import get_coreclr_command_line + + coreclr = get_coreclr_command_line(entry_dll=example_netcore / "example.dll") + asm = coreclr.get_assembly(example_netcore / "example.dll") + + run_tests(asm) + + def test_coreclr_properties(example_netcore: Path): run_in_subprocess( _do_test_coreclr_autogenerated_runtimeconfig,