diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6f9e75..41f4e0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.10', '3.9', '3.8', '3.7'] # pypy3 + python: ['3.11', '3.10', '3.9', '3.8'] # pypy3 steps: - uses: actions/checkout@v3 diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 4b08148..aa604ad 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -70,7 +70,7 @@ def get_mono( libmono = _maybe_path(libmono) if libmono is None: - libmono = find_libmono(sgen=sgen) + libmono = find_libmono(sgen=sgen, assembly_dir=assembly_dir) impl = Mono( # domain=domain, diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index 522fec5..225b4c7 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -20,9 +20,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._is_initialized = False self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config) - self._load_func = _get_load_func(self._dll, self._handle) + self._load_func = None for key, value in params.items(): self[key] = value @@ -36,7 +35,7 @@ def dotnet_root(self) -> Path: @property def is_initialized(self) -> bool: - return self._is_initialized + return self._load_func is not None @property def is_shutdown(self) -> bool: @@ -81,10 +80,15 @@ def __iter__(self) -> Generator[Tuple[str, str], None, None]: for i in range(size_ptr[0]): yield (decode(keys_ptr[i]), decode(values_ptr[i])) + def _get_load_func(self): + if self._load_func is None: + self._load_func = _get_load_func(self._dll, self._handle) + + return self._load_func + def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): # TODO: Maybe use coreclr_get_delegate as well, supported with newer API # versions of hostfxr - self._is_initialized = True # Append assembly name to typename assembly_path = Path(assembly_path) @@ -92,7 +96,7 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): typename = f"{typename}, {assembly_name}" delegate_ptr = ffi.new("void**") - res = self._load_func( + res = self._get_load_func()( encode(str(assembly_path)), encode(typename), encode(function), @@ -103,12 +107,6 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): check_result(res) return ffi.cast("component_entry_point_fn", delegate_ptr[0]) - def _check_initialized(self) -> None: - if self._handle is None: - raise RuntimeError("Runtime is shut down") - elif not self._is_initialized: - raise RuntimeError("Runtime is not initialized") - def shutdown(self) -> None: if self._handle is not None: self._dll.hostfxr_close(self._handle) diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 158ddb7..7c3f20d 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -136,7 +136,7 @@ def initialize( _MONO = load_mono(libmono) if assembly_dir is not None and config_dir is not None: - _MONO.mono_set_dirs(assembly_dir, config_dir) + _MONO.mono_set_dirs(assembly_dir.encode("utf8"), config_dir.encode("utf8")) # Load in global config (i.e /etc/mono/config) global_encoded = global_config_file or ffi.NULL diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 24460ca..4efe841 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -14,13 +14,15 @@ def __init__( ): initialize() if config_file is not None: - config_file_s = str(config_file) + config_file_s = str(config_file).encode("utf8") else: config_file_s = ffi.NULL + domain_s = domain.encode("utf8") if domain else ffi.NULL + self._domain_name = domain self._config_file = config_file - self._domain = _FW.pyclr_create_appdomain(domain or ffi.NULL, config_file_s) + self._domain = _FW.pyclr_create_appdomain(domain_s, config_file_s) def info(self) -> RuntimeInfo: return RuntimeInfo( @@ -41,6 +43,11 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): function.encode("utf8"), ) + if func == ffi.NULL: + raise RuntimeError( + f"Failed to resolve {typename}.{function} from {assembly_path}" + ) + return func def shutdown(self): diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index d5d7b89..6ef7bc3 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -105,7 +105,7 @@ def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: return find_runtimes_in_root(dotnet_root) -def find_libmono(*, sgen: bool = True) -> Path: +def find_libmono(*, assembly_dir: str = None, sgen: bool = True) -> Path: """Find a suitable libmono dynamic library On Windows and macOS, we check the default installation directories. @@ -137,9 +137,12 @@ def find_libmono(*, sgen: bool = True) -> Path: ) else: - from ctypes.util import find_library - - path = find_library(unix_name) + if assembly_dir == None: + from ctypes.util import find_library + path = find_library(unix_name) + else: + libname = "lib" + unix_name + ".so" + path = Path(assembly_dir) / "lib" / libname if path is None: raise RuntimeError("Could not find libmono") diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index e8b2767..32b4c01 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -32,8 +32,10 @@ public static IntPtr CreateAppDomain( { var setup = new AppDomainSetup { + ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, ConfigurationFile = configFile }; + Print($"Base: {AppDomain.CurrentDomain.BaseDirectory}"); var domain = AppDomain.CreateDomain(name, null, setup); Print($"Located domain {domain}"); @@ -51,9 +53,9 @@ public static IntPtr CreateAppDomain( [DllExport("pyclr_get_function", CallingConvention.Cdecl)] public static IntPtr GetFunction( IntPtr domain, - [MarshalAs(UnmanagedType.LPStr)] string assemblyPath, - [MarshalAs(UnmanagedType.LPStr)] string typeName, - [MarshalAs(UnmanagedType.LPStr)] string function + [MarshalAs(UnmanagedType.LPUTF8Str)] string assemblyPath, + [MarshalAs(UnmanagedType.LPUTF8Str)] string typeName, + [MarshalAs(UnmanagedType.LPUTF8Str)] string function ) { try diff --git a/tests/test_common.py b/tests/test_common.py index f42020a..8a9e36d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,3 +1,4 @@ +import shutil import pytest from subprocess import check_call import os @@ -17,14 +18,14 @@ def example_netcore(tmpdir_factory): def build_example(tmpdir_factory, framework): out = Path(tmpdir_factory.mktemp(f"example-{framework}")) - proj_path = Path(__file__).parent.parent / "example" + proj_path = Path(__file__).parent.parent / "example" / "example.csproj" check_call(["dotnet", "build", str(proj_path), "-o", str(out), "-f", framework]) return out -def test_mono(example_netstandard): +def test_mono(example_netstandard: Path): from clr_loader import get_mono mono = get_mono() @@ -33,7 +34,7 @@ def test_mono(example_netstandard): run_tests(asm) -def test_mono_debug(example_netstandard): +def test_mono_debug(example_netstandard: Path): from clr_loader import get_mono mono = get_mono( @@ -46,7 +47,8 @@ def test_mono_debug(example_netstandard): run_tests(asm) -def test_mono_signal_chaining(example_netstandard): + +def test_mono_signal_chaining(example_netstandard: Path): from clr_loader import get_mono mono = get_mono(set_signal_chaining=True) @@ -54,7 +56,8 @@ def test_mono_signal_chaining(example_netstandard): run_tests(asm) -def test_mono_set_dir(example_netstandard): + +def test_mono_set_dir(example_netstandard: Path): from clr_loader import get_mono mono = get_mono(assembly_dir="/usr/lib", config_dir="/etc") @@ -62,7 +65,8 @@ def test_mono_set_dir(example_netstandard): run_tests(asm) -def test_coreclr(example_netcore): + +def test_coreclr(example_netcore: Path): from clr_loader import get_coreclr coreclr = get_coreclr(runtime_config=example_netcore / "example.runtimeconfig.json") @@ -71,21 +75,24 @@ def test_coreclr(example_netcore): run_tests(asm) -def test_coreclr_autogenerated_runtimeconfig(example_netstandard): - from multiprocessing import get_context - - p = get_context("spawn").Process( - target=_do_test_coreclr_autogenerated_runtimeconfig, args=(example_netstandard,) +def test_coreclr_properties(example_netcore: Path): + run_in_subprocess( + _do_test_coreclr_autogenerated_runtimeconfig, + example_netstandard, + properties=dict(APP_CONTEXT_BASE_DIRECTORY=str(example_netcore)), ) - p.start() - p.join() - p.close() -def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard): +def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): + run_in_subprocess(_do_test_coreclr_autogenerated_runtimeconfig, example_netstandard) + + +def _do_test_coreclr_autogenerated_runtimeconfig( + example_netstandard: Path, **properties +): from clr_loader import get_coreclr - coreclr = get_coreclr() + coreclr = get_coreclr(properties=properties) asm = coreclr.get_assembly(example_netstandard / "example.dll") run_tests(asm) @@ -94,11 +101,32 @@ def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard): @pytest.mark.skipif( sys.platform != "win32", reason=".NET Framework only exists on Windows" ) -def test_netfx(example_netstandard): +def test_netfx(example_netstandard: Path): + run_in_subprocess(_do_test_netfx, example_netstandard) + + +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_chinese_path(example_netstandard: Path, tmpdir_factory): + tmp_path = Path(tmpdir_factory.mktemp("example-中国")) + shutil.copytree(example_netstandard, tmp_path, dirs_exist_ok=True) + + run_in_subprocess(_do_test_netfx, tmp_path) + + +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_separate_domain(example_netstandard): + run_in_subprocess(_do_test_netfx, example_netstandard, domain="some domain") + + +def _do_test_netfx(example_netstandard, **kwargs): from clr_loader import get_netfx - netfx = get_netfx() - asm = netfx.get_assembly(os.path.join(example_netstandard, "example.dll")) + netfx = get_netfx(**kwargs) + asm = netfx.get_assembly(example_netstandard / "example.dll") run_tests(asm) @@ -108,3 +136,12 @@ def run_tests(asm): test_data = b"testy mctestface" res = func(test_data) assert res == len(test_data) + + +def run_in_subprocess(func, *args, **kwargs): + from multiprocessing import get_context + + p = get_context("spawn").Process(target=func, args=args, kwargs=kwargs) + p.start() + p.join() + p.close()