Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 46143d4

Browse files
authored
Refactoring to improve API and allow configless .NET Core loading (#31)
- Drop `Runtime` wrapper in favour of an abstract base class - Use `pathlib` throughout and allow it as parameter - Expose a `shutdown` method (that is a noop in most cases, but at least it exists) - Add functions to find all installed .NET Core runtimes - If no runtime configuration is given, generate a simple one in a temporary directory
1 parent f6cc833 commit 46143d4

13 files changed

+457
-188
lines changed

clr_loader/__init__.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,106 @@
1+
from pathlib import Path
2+
from tempfile import TemporaryDirectory
13
from typing import Dict, Optional, Sequence
24

3-
from .wrappers import Runtime
4-
from .util.find import find_libmono, find_dotnet_root
5+
from .types import Assembly, Runtime, RuntimeInfo
6+
from .util import StrOrPath
7+
from .util.find import find_dotnet_root, find_libmono, find_runtimes
8+
from .util.runtime_spec import DotnetCoreRuntimeSpec
59

6-
__all__ = ["get_mono", "get_netfx", "get_coreclr"]
10+
__all__ = [
11+
"get_mono",
12+
"get_netfx",
13+
"get_coreclr",
14+
"find_dotnet_root",
15+
"find_libmono",
16+
"find_runtimes",
17+
"Runtime",
18+
"Assembly",
19+
"RuntimeInfo",
20+
]
721

822

923
def get_mono(
24+
*,
1025
domain: Optional[str] = None,
11-
config_file: Optional[str] = None,
12-
global_config_file: Optional[str] = None,
13-
libmono: Optional[str] = None,
26+
config_file: Optional[StrOrPath] = None,
27+
global_config_file: Optional[StrOrPath] = None,
28+
libmono: Optional[StrOrPath] = None,
1429
sgen: bool = True,
1530
debug: bool = False,
1631
jit_options: Optional[Sequence[str]] = None,
1732
) -> Runtime:
1833
from .mono import Mono
1934

35+
libmono = _maybe_path(libmono)
2036
if libmono is None:
2137
libmono = find_libmono(sgen)
2238

2339
impl = Mono(
2440
domain=domain,
2541
debug=debug,
2642
jit_options=jit_options,
27-
config_file=config_file,
28-
global_config_file=global_config_file,
43+
config_file=_maybe_path(config_file),
44+
global_config_file=_maybe_path(global_config_file),
2945
libmono=libmono,
3046
)
31-
return Runtime(impl)
47+
return impl
3248

3349

3450
def get_coreclr(
35-
runtime_config: str,
36-
dotnet_root: Optional[str] = None,
51+
*,
52+
runtime_config: Optional[StrOrPath] = None,
53+
dotnet_root: Optional[StrOrPath] = None,
3754
properties: Optional[Dict[str, str]] = None,
55+
runtime_spec: Optional[DotnetCoreRuntimeSpec] = None,
3856
) -> Runtime:
3957
from .hostfxr import DotnetCoreRuntime
4058

59+
dotnet_root = _maybe_path(dotnet_root)
4160
if dotnet_root is None:
4261
dotnet_root = find_dotnet_root()
4362

63+
temp_dir = None
64+
runtime_config = _maybe_path(runtime_config)
65+
if runtime_config is None:
66+
if runtime_spec is None:
67+
candidates = [
68+
rt for rt in find_runtimes() if rt.name == "Microsoft.NETCore.App"
69+
]
70+
candidates.sort(key=lambda spec: spec.version, reverse=True)
71+
if not candidates:
72+
raise RuntimeError("Failed to find a suitable runtime")
73+
74+
runtime_spec = candidates[0]
75+
76+
temp_dir = TemporaryDirectory()
77+
runtime_config = Path(temp_dir.name) / "runtimeconfig.json"
78+
79+
with open(runtime_config, "w") as f:
80+
runtime_spec.write_config(f)
81+
4482
impl = DotnetCoreRuntime(runtime_config=runtime_config, dotnet_root=dotnet_root)
4583
if properties:
4684
for key, value in properties.items():
4785
impl[key] = value
4886

49-
return Runtime(impl)
87+
if temp_dir:
88+
temp_dir.cleanup()
5089

90+
return impl
5191

52-
def get_netfx(name: Optional[str] = None, config_file: Optional[str] = None) -> Runtime:
92+
93+
def get_netfx(
94+
*, name: Optional[str] = None, config_file: Optional[StrOrPath] = None
95+
) -> Runtime:
5396
from .netfx import NetFx
5497

55-
impl = NetFx(name=name, config_file=config_file)
56-
return Runtime(impl)
98+
impl = NetFx(name=name, config_file=_maybe_path(config_file))
99+
return impl
100+
101+
102+
def _maybe_path(p: Optional[StrOrPath]) -> Optional[Path]:
103+
if p is None:
104+
return None
105+
else:
106+
return Path(p)

clr_loader/ffi/__init__.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import glob
2-
import os
31
import sys
2+
from pathlib import Path
43
from typing import Optional
54

65
import cffi # type: ignore
@@ -9,46 +8,51 @@
98

109
__all__ = ["ffi", "load_hostfxr", "load_mono", "load_netfx"]
1110

12-
ffi = cffi.FFI()
11+
ffi = cffi.FFI() # type: ignore
1312

1413
for cdef in hostfxr.cdef + mono.cdef + netfx.cdef:
1514
ffi.cdef(cdef)
1615

1716

18-
def load_hostfxr(dotnet_root: str):
17+
def load_hostfxr(dotnet_root: Path):
1918
hostfxr_name = _get_dll_name("hostfxr")
20-
hostfxr_path = os.path.join(dotnet_root, "host", "fxr", "?.*", hostfxr_name)
2119

22-
for hostfxr_path in reversed(sorted(glob.glob(hostfxr_path))):
20+
# This will fail as soon as .NET hits version 10, but hopefully by then
21+
# we'll have a more robust way of finding the libhostfxr
22+
hostfxr_path = dotnet_root / "host" / "fxr"
23+
hostfxr_paths = hostfxr_path.glob(f"?.*/{hostfxr_name}")
24+
25+
for hostfxr_path in reversed(sorted(hostfxr_paths)):
2326
try:
24-
return ffi.dlopen(hostfxr_path)
27+
return ffi.dlopen(str(hostfxr_path))
2528
except Exception:
2629
pass
2730

2831
raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}")
2932

3033

31-
def load_mono(path: Optional[str] = None):
34+
def load_mono(path: Optional[Path] = None):
3235
# Preload C++ standard library, Mono needs that and doesn't properly link against it
33-
if sys.platform.startswith("linux"):
36+
if sys.platform == "linux":
3437
ffi.dlopen("stdc++", ffi.RTLD_GLOBAL)
3538

36-
return ffi.dlopen(path, ffi.RTLD_GLOBAL)
39+
path_str = str(path) if path else None
40+
return ffi.dlopen(path_str, ffi.RTLD_GLOBAL)
3741

3842

3943
def load_netfx():
4044
if sys.platform != "win32":
4145
raise RuntimeError(".NET Framework is only supported on Windows")
4246

43-
dirname = os.path.join(os.path.dirname(__file__), "dlls")
47+
dirname = Path(__file__).parent / "dlls"
4448
if sys.maxsize > 2**32:
4549
arch = "amd64"
4650
else:
4751
arch = "x86"
4852

49-
path = os.path.join(dirname, arch, "ClrLoader.dll")
53+
path = dirname / arch / "ClrLoader.dll"
5054

51-
return ffi.dlopen(path)
55+
return ffi.dlopen(str(path))
5256

5357

5458
def _get_dll_name(name: str) -> str:

clr_loader/ffi/hostfxr.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44

5+
56
cdef = []
67

78
if sys.platform == "win32":

clr_loader/hostfxr.py

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
1-
import os
21
import sys
2+
from pathlib import Path
3+
from typing import Generator, Tuple
34

45
from .ffi import ffi, load_hostfxr
5-
from .util import check_result, find_dotnet_root
6+
from .types import Runtime, RuntimeInfo, StrOrPath
7+
from .util import check_result
68

79
__all__ = ["DotnetCoreRuntime"]
810

11+
_IS_SHUTDOWN = False
912

10-
class DotnetCoreRuntime:
11-
def __init__(self, runtime_config: str, dotnet_root: str):
12-
self._dotnet_root = dotnet_root or find_dotnet_root()
13+
14+
class DotnetCoreRuntime(Runtime):
15+
def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str):
16+
if _IS_SHUTDOWN:
17+
raise RuntimeError("Runtime can not be reinitialized")
18+
19+
self._dotnet_root = Path(dotnet_root)
1320
self._dll = load_hostfxr(self._dotnet_root)
14-
self._is_finalized = False
21+
self._is_initialized = False
1522
self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config)
1623
self._load_func = _get_load_func(self._dll, self._handle)
1724

25+
for key, value in params.items():
26+
self[key] = value
27+
28+
# TODO: Get version
29+
self._version = "<undefined>"
30+
1831
@property
19-
def dotnet_root(self) -> str:
32+
def dotnet_root(self) -> Path:
2033
return self._dotnet_root
2134

2235
@property
23-
def is_finalized(self) -> bool:
24-
return self._is_finalized
36+
def is_initialized(self) -> bool:
37+
return self._is_initialized
38+
39+
@property
40+
def is_shutdown(self) -> bool:
41+
return _IS_SHUTDOWN
2542

2643
def __getitem__(self, key: str) -> str:
44+
if self.is_shutdown:
45+
raise RuntimeError("Runtime is shut down")
2746
buf = ffi.new("char_t**")
2847
res = self._dll.hostfxr_get_runtime_property_value(
2948
self._handle, encode(key), buf
@@ -34,15 +53,17 @@ def __getitem__(self, key: str) -> str:
3453
return decode(buf[0])
3554

3655
def __setitem__(self, key: str, value: str) -> None:
37-
if self.is_finalized:
38-
raise RuntimeError("Already finalized")
56+
if self.is_initialized:
57+
raise RuntimeError("Already initialized")
3958

4059
res = self._dll.hostfxr_set_runtime_property_value(
4160
self._handle, encode(key), encode(value)
4261
)
4362
check_result(res)
4463

45-
def __iter__(self):
64+
def __iter__(self) -> Generator[Tuple[str, str], None, None]:
65+
if self.is_shutdown:
66+
raise RuntimeError("Runtime is shut down")
4667
max_size = 100
4768
size_ptr = ffi.new("size_t*")
4869
size_ptr[0] = max_size
@@ -51,25 +72,26 @@ def __iter__(self):
5172
values_ptr = ffi.new("char_t*[]", max_size)
5273

5374
res = self._dll.hostfxr_get_runtime_properties(
54-
self._dll._handle, size_ptr, keys_ptr, values_ptr
75+
self._handle, size_ptr, keys_ptr, values_ptr
5576
)
5677
check_result(res)
5778

5879
for i in range(size_ptr[0]):
5980
yield (decode(keys_ptr[i]), decode(values_ptr[i]))
6081

61-
def get_callable(self, assembly_path: str, typename: str, function: str):
82+
def get_callable(self, assembly_path: StrOrPath, typename: str, function: str):
6283
# TODO: Maybe use coreclr_get_delegate as well, supported with newer API
6384
# versions of hostfxr
64-
self._is_finalized = True
85+
self._is_initialized = True
6586

6687
# Append assembly name to typename
67-
assembly_name, _ = os.path.splitext(os.path.basename(assembly_path))
88+
assembly_path = Path(assembly_path)
89+
assembly_name = assembly_path.stem
6890
typename = f"{typename}, {assembly_name}"
6991

7092
delegate_ptr = ffi.new("void**")
7193
res = self._load_func(
72-
encode(assembly_path),
94+
encode(str(assembly_path)),
7395
encode(typename),
7496
encode(function),
7597
ffi.NULL,
@@ -79,27 +101,39 @@ def get_callable(self, assembly_path: str, typename: str, function: str):
79101
check_result(res)
80102
return ffi.cast("component_entry_point_fn", delegate_ptr[0])
81103

104+
def _check_initialized(self) -> None:
105+
if self._handle is None:
106+
raise RuntimeError("Runtime is shut down")
107+
elif not self._is_initialized:
108+
raise RuntimeError("Runtime is not initialized")
109+
82110
def shutdown(self) -> None:
83111
if self._handle is not None:
84112
self._dll.hostfxr_close(self._handle)
85113
self._handle = None
86114

87-
def __del__(self):
88-
self.shutdown()
115+
def info(self):
116+
return RuntimeInfo(
117+
kind="CoreCLR",
118+
version=self._version,
119+
initialized=self._handle is not None,
120+
shutdown=self._handle is None,
121+
properties=dict(self) if not _IS_SHUTDOWN else {},
122+
)
89123

90124

91-
def _get_handle(dll, dotnet_root: str, runtime_config: str):
125+
def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath):
92126
params = ffi.new("hostfxr_initialize_parameters*")
93127
params.size = ffi.sizeof("hostfxr_initialize_parameters")
94128
# params.host_path = ffi.new("char_t[]", encode(sys.executable))
95129
params.host_path = ffi.NULL
96-
dotnet_root_p = ffi.new("char_t[]", encode(dotnet_root))
130+
dotnet_root_p = ffi.new("char_t[]", encode(str(Path(dotnet_root))))
97131
params.dotnet_root = dotnet_root_p
98132

99133
handle_ptr = ffi.new("hostfxr_handle*")
100134

101135
res = dll.hostfxr_initialize_for_runtime_config(
102-
encode(runtime_config), params, handle_ptr
136+
encode(str(Path(runtime_config))), params, handle_ptr
103137
)
104138
check_result(res)
105139

@@ -119,16 +153,16 @@ def _get_load_func(dll, handle):
119153

120154
if sys.platform == "win32":
121155

122-
def encode(string):
156+
def encode(string: str):
123157
return string
124158

125-
def decode(char_ptr):
159+
def decode(char_ptr) -> str:
126160
return ffi.string(char_ptr)
127161

128162
else:
129163

130-
def encode(string):
164+
def encode(string: str):
131165
return string.encode("utf8")
132166

133-
def decode(char_ptr):
167+
def decode(char_ptr) -> str:
134168
return ffi.string(char_ptr).decode("utf8")

0 commit comments

Comments
 (0)