1
0
Fork 0
mirror of https://github.com/anrieff/libcpuid synced 2025-07-02 14:04:15 +00:00

Add Python bindings

The bindings are implemented via python-cffi and cover all
current functionality of the library. They do not provide
a 1-to-1 mapping of the functionality, but a more "Pythonic"
API instead.

When new functionality is added to the libcpuid library
in the future, the changes must be manually added to the
bindings in order to appear in the Python interface.
However, if only a C enum is extended, the changes will
be automatically reflected in the Python interface.
This commit is contained in:
Pavol Žáčik 2024-07-23 17:52:51 +02:00 committed by The Tumultuous Unicorn Of Darkness
parent 6c87edab5a
commit 40e2d5fcb6
14 changed files with 1133 additions and 0 deletions

12
python/pyproject.toml Normal file
View file

@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools", "cffi"]
build-backend = "setuptools.build_meta"
[project]
name = "libcpuid"
version = "0.1.0"
readme = "README.md"
dependencies = ["cffi"]
license = {text = "BSD-3-Clause"}
authors = [{name = "Pavol Žáčik", email = "zacikpa@gmail.com"}]
description = "Python bindings for the libcpuid C library"

5
python/setup.py Normal file
View file

@ -0,0 +1,5 @@
from setuptools import setup
setup(
cffi_modules=["src/libcpuid/_ffi_build.py:ffibuilder"],
)

View file

@ -0,0 +1,68 @@
"""
The libcpuid package defines Python bindings to the
libcpuid C library, which provides CPU identification.
"""
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
ffi,
lib,
)
from libcpuid import enums
from libcpuid.errors import CLibraryError
from libcpuid._utils import c_string_to_str
def version() -> str:
"""Returns the version of the libcpuid library."""
return c_string_to_str(lib.cpuid_lib_version())
def cpuid_present() -> bool:
"""Checks if the `cpuid` instruction is supported."""
return bool(lib.cpuid_present())
def get_total_cpus() -> int:
"""Returns the total number of logical CPU threads."""
return lib.cpuid_get_total_cpus()
def get_vendor() -> enums.CPUVendor:
"""Returns the CPU vendor of the current CPU."""
try:
return enums.CPUVendor(lib.cpuid_get_vendor())
except ValueError:
return enums.CPUVendor.UNKNOWN
def get_cpu_list(vendor: enums.CPUVendor) -> list[str]:
"""Gets a list of CPU :meth:`codenames <info.CPUInfo.cpu_codename>` for a specific vendor."""
c_cpu_list = ffi.new("struct cpu_list_t *")
lib.cpuid_get_cpu_list(vendor, c_cpu_list)
if c_cpu_list.num_entries == 0:
raise CLibraryError
cpu_list = [
c_string_to_str(name) for name in c_cpu_list.names[0 : c_cpu_list.num_entries]
]
lib.cpuid_free_cpu_list(c_cpu_list)
return cpu_list
def exec_cpuid(
eax: bytes,
ebx: bytes = 4 * b"\x00",
ecx: bytes = 4 * b"\x00",
edx: bytes = 4 * b"\x00",
) -> tuple[bytes, bytes, bytes, bytes]:
"""Executes the `cpuid` instruction with the given content of input registers."""
regs = ffi.new(
"uint32_t[4]",
[
int.from_bytes(eax),
int.from_bytes(ebx),
int.from_bytes(ecx),
int.from_bytes(edx),
],
)
lib.cpu_exec_cpuid_ext(regs)
return tuple(reg.to_bytes(4) for reg in regs)

View file

@ -0,0 +1,25 @@
"""
Module for compiling the C FFI.
"""
import os
import sys
from cffi import FFI
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from _ffi_build_utils import ( # pylint: disable=import-error, wrong-import-position
get_include_flags,
find_header_file,
preprocess_header,
eval_sizeofs,
)
include_flags = get_include_flags()
preprocessed_header = preprocess_header(find_header_file(include_flags))
no_sizeof_header = eval_sizeofs(preprocessed_header, include_flags)
ffibuilder = FFI()
ffibuilder.cdef(no_sizeof_header)
ffibuilder.set_source_pkgconfig(
"libcpuid._libcpuid_cffi", ["libcpuid"], "#include <libcpuid.h>"
)

View file

@ -0,0 +1,105 @@
"""
Utility functions for building the FFI.
"""
import subprocess
import os
import re
import tempfile
class FFIBuildException(Exception):
"""Generic exception for errors occuring during the CFFI build."""
def get_include_flags():
"""
Obtains libcpuid include flags via pkg-config.
"""
try:
cflags = (
subprocess.check_output(["pkg-config", "libcpuid", "--cflags-only-I"])
.decode()
.strip()
.split()
)
return cflags
except subprocess.CalledProcessError as e:
if e.returncode == 127:
raise FFIBuildException(
"The pkg-config command is necessary to build python-libcpuid."
) from e
if e.returncode == 1:
raise FFIBuildException(
"The libcpuid C library (devel) was not found."
) from e
raise FFIBuildException("Error looking for the libcpuid library") from e
def find_header_file(include_flags):
"""
Obtains main libcpuid header file location from include flags.
"""
header_path = None # pylint: disable=invalid-name
for cflag in include_flags:
header_candidate = os.path.join(cflag[2:], "libcpuid.h")
if os.path.isfile(header_candidate):
header_path = header_candidate
break
if header_path is None:
raise FFIBuildException("Could not find header file of the libcpuid library.")
return header_path
def preprocess_header(header_path):
"""
Preprocesses the header file (python-cffi only accepts preprocessed C definitions)
at the given path and returns it as a string.
"""
try:
return subprocess.check_output(
["gcc", "-U __GNUC__", "-E", header_path]
).decode()
except subprocess.CalledProcessError as e:
if e.returncode == 127:
raise FFIBuildException(
"The gcc compiler is necessary to build python-libcpuid."
) from e
raise FFIBuildException(
f"Error preprocessing the libcpuid header file: {e.stderr}"
) from e
def _get_sizeof_eval_source(sizeof):
return f"""
#include <libcpuid.h>
#include <stdio.h>
int main() {{
printf("%ld", {sizeof});
return 0;
}}
"""
def eval_sizeofs(header, cflags):
"""
Evaluates each sizeof found in the given C header and replaces all
occurences of the sizeof with its computed value.
"""
sizeofs = set(re.findall(r"sizeof\([^\)]*\)", header))
tmp_dir = tempfile.mkdtemp()
c_program_path = os.path.join(tmp_dir, "sizeof.c")
executable_path = os.path.join(tmp_dir, "sizeof")
for sizeof in sizeofs:
with open(c_program_path, "w", encoding="UTF-8") as c_program_file:
c_program_file.write(_get_sizeof_eval_source(sizeof))
subprocess.check_call(["gcc", c_program_path, *cflags, "-o", executable_path])
size = subprocess.check_output([executable_path]).decode()
header = header.replace(sizeof, size)
os.remove(c_program_path)
os.remove(executable_path)
os.rmdir(tmp_dir)
return header

View file

@ -0,0 +1,31 @@
"""
Internal module containing utility functions.
"""
from typing import Optional
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
ffi,
lib,
)
def c_string_to_str(c_string) -> str:
"""Converts an FFI C string to a Python string."""
return ffi.string(c_string).decode()
def optional_int(value: int) -> Optional[int]:
"""Returns the given integer if it is not -1, otherwise None."""
return value if value != -1 else None
def get_enum_options( # pylint: disable=dangerous-default-value
prefix, ignore=[]
) -> dict[str, int]:
"""Returns a dictionary of C enum option names and their values based on name prefix."""
enum_options = [x for x in dir(lib) if x.startswith(prefix)]
return {
x[len(prefix) :]: getattr(lib, x)
for x in enum_options
if x[len(prefix) :] not in ignore
}

View file

@ -0,0 +1,123 @@
"""
Module for CPU clock/frequency calculation.
"""
from enum import Enum, auto
from libcpuid.errors import LibcpuidError
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
ffi,
)
def exec_rdtsc() -> int:
"""
Exectutes the `rdtsc` (read timestamp counter) instruction
and returns the value of the counter.
"""
result = ffi.new("uint64_t *")
lib.cpu_rdtsc(result)
return result[0]
class TSC:
"""
Class for calculating clock frequency using differences
in time and in the timestamp counter.
"""
class State(Enum):
"""State of the measurement."""
NOT_STARTED = auto()
STARTED = auto()
STOPPED = auto()
def __init__(self):
self._c_cpu_mark = ffi.new("struct cpu_mark_t *")
self._state = self.State.NOT_STARTED
@property
def state(self) -> State:
"""Returns the current state of the measurement."""
return self._state
def start(self):
"""Starts the measurement."""
lib.cpu_tsc_mark(self._c_cpu_mark)
self._state = self.State.STARTED
def stop(self):
"""Stops the measurement."""
if self._state == self.State.NOT_STARTED:
raise LibcpuidError("The measurement has not been started.")
if self._state == self.State.STOPPED:
raise LibcpuidError("The measurement has already been stopped.")
lib.cpu_tsc_unmark(self._c_cpu_mark)
self._state = self.State.STOPPED
def frequency(self) -> int:
"""
Calculates the CPU clock frequency from the time and TSC
difference between the calls to :meth:`start` and :meth:`stop`.
"""
if self._state == self.State.NOT_STARTED:
raise LibcpuidError("The measurementhas not been started.")
if self._state == self.State.STARTED:
raise LibcpuidError("The measurement has not been stopped.")
result = lib.cpu_clock_by_mark(self._c_cpu_mark)
if result < 0:
raise LibcpuidError("Could not compute clock frequency.")
return result
def frequency_measure(millis: int = 100, quad_check: bool = False) -> int:
"""
Measures the CPU clock frequency by busy-waiting for a specified
number of milliseconds and calculating the difference between TSC
values. With the :const:`quad_check` option on, the function will
run 4 consecutive measurements and return the average of the two
most consistent results.
"""
result = lib.cpu_clock_measure(millis, int(quad_check))
if result < 0:
raise LibcpuidError("Could not measure clock frequency.")
return result
def frequency_by_os() -> int:
"""Returns the CPU clock frequency as reported by the OS."""
result = lib.cpu_clock_by_os()
if result < 0:
raise LibcpuidError("Clock frequency unavailable.")
return result
def frequency_by_ic(millis: int = 50, runs: int = 4) -> int:
"""
Measures the CPU clock frequency using instruction counting for
a specified number of milliseconds and runs.
"""
result = lib.cpu_clock_by_ic(millis, runs)
if result < 0:
raise LibcpuidError("Could not measure clock frequency.")
return result
def frequency_by_tsc() -> int:
"""Computes the CPU clock **base** frequency based on information from `cpuid`."""
result = lib.cpu_clock_by_tsc(ffi.NULL)
if result < 0:
raise LibcpuidError("Could not compute clock frequency.")
return result
def frequency() -> int:
"""
All in one method for getting CPU clock frequency. It tries to use the OS
and falls back to other methods if the OS does not have the information.
"""
result = lib.cpu_clock()
if result < 0:
raise LibcpuidError("Could not get clock frequency.")
return result

View file

@ -0,0 +1,54 @@
"""
Module providing access to enums from the libcpuid library,
e.g., CPU architectures, vendors, or features.
For compatibility with the underlying C library, the values
of all enums are integers. However, calling the :func:`str`
or :func:`print` function on an enum member returns or prints
its string representation.
"""
from enum import IntEnum
from libcpuid._utils import c_string_to_str, get_enum_options
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
)
CPUArchitecture = IntEnum("CPUArchitecture", get_enum_options("ARCHITECTURE_"))
CPUArchitecture.__str__ = lambda self: c_string_to_str(lib.cpu_architecture_str(self))
CPUArchitecture.__doc__ = "CPU architectures."
CPUVendor = IntEnum("CPUVendor", get_enum_options("VENDOR_"))
CPUVendor.__str__ = lambda self: self.name
CPUVendor.__doc__ = "CPU vendors."
CPUPurpose = IntEnum("CPUPurpose", get_enum_options("PURPOSE_"))
CPUPurpose.__str__ = lambda self: c_string_to_str(lib.cpu_purpose_str(self))
CPUPurpose.__doc__ = "CPU purposes."
HypervisorVendor = IntEnum(
"HypervisorVendor", get_enum_options("HYPERVISOR_", ignore=["NONE"])
)
HypervisorVendor.__str__ = lambda self: self.name
HypervisorVendor.__doc__ = (
"Hypervisor vendors, as guessed from the CPU_FEATURE_HYPERVISOR flag"
)
CPUFeature = IntEnum("CPUFeature", get_enum_options("CPU_FEATURE_"))
CPUFeature.__str__ = lambda self: c_string_to_str(lib.cpu_feature_str(self))
CPUFeature.__doc__ = "CPU feature identifiers (flags)"
CPUFeatureLevel = IntEnum("CPUFeatureLevel", get_enum_options("FEATURE_LEVEL_"))
CPUFeatureLevel.__str__ = lambda self: c_string_to_str(lib.cpu_feature_level_str(self))
CPUFeatureLevel.__doc__ = """
CPU feature levels, also known as microarchitecture levels (x86) or architecture versions (ARM).
"""
CPUHint = IntEnum("CPUHint", get_enum_options("CPU_HINT_"))
CPUHint.__str__ = lambda self: self.name
CPUHint.__doc__ = "CPU detection hint identifiers."
SGXFeature = IntEnum("SGXFeature", get_enum_options("INTEL_"))
SGXFeature.__str__ = lambda self: self.name
SGXFeature.__doc__ = "SGX feature flags."

View file

@ -0,0 +1,22 @@
"""
Module containing custom exceptions of the library.
"""
from libcpuid._utils import c_string_to_str
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
)
class LibcpuidError(Exception):
"""Main exception of the library."""
class CLibraryError(LibcpuidError):
"""
Raised when an error code is returned by some function
in the underlying libcpuid C library.
"""
def __init__(self):
super().__init__(c_string_to_str(lib.cpuid_error()))

419
python/src/libcpuid/info.py Normal file
View file

@ -0,0 +1,419 @@
"""
The main module, providing a class that holds CPU information.
"""
from typing import Optional
from libcpuid import enums
from libcpuid.sgx import SGX
from libcpuid.raw import CPURawData, CPURawDataArray
from libcpuid.errors import CLibraryError
from libcpuid._utils import c_string_to_str, optional_int
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
ffi,
)
class CPUInfo: # pylint: disable=too-many-public-methods
"""
The main class, holding information about a CPU
in human-friendly format.
"""
def __init__(self, c_cpu_id):
self._c_cpu_id = c_cpu_id
self._features = None
self._detection_hints = None
@classmethod
def from_c(cls, c_cpu_id):
"""Creates a :class:`CPUInfo` instance from the corresponding C structure."""
cpu_info = cls(c_cpu_id)
if cpu_info.architecture == enums.CPUArchitecture.X86:
return X86Info(c_cpu_id)
if cpu_info.architecture == enums.CPUArchitecture.ARM:
return ARMInfo(c_cpu_id)
return cpu_info
@classmethod
def from_current_cpu(cls):
"""Creates a :class:`CPUInfo` instance by identifying the current CPU."""
c_cpu_id = ffi.new("struct cpu_id_t *")
if lib.cpu_identify(ffi.NULL, c_cpu_id) != 0:
raise CLibraryError
return cls.from_c(c_cpu_id)
@classmethod
def from_purpose(cls, purpose: enums.CPUPurpose):
"""Creates a :class:`CPUInfo` instance by identifying a CPU with the requested purpose."""
c_cpu_id = ffi.new("struct cpu_id_t *")
if lib.cpu_request_core_type(purpose, ffi.NULL, c_cpu_id) != 0:
raise CLibraryError
return CPUInfo.from_c(c_cpu_id)
@classmethod
def from_raw(cls, raw_data: CPURawData):
"""Creates a :class:`CPUInfo` instance from an instance of :class:`CPURawData`"""
c_cpu_id = ffi.new("struct cpu_id_t *")
if lib.cpu_identify(raw_data.c_cpu_raw_data, c_cpu_id) != 0:
raise CLibraryError
return CPUInfo.from_c(c_cpu_id)
@classmethod
def from_raw_array_and_purpose(
cls, raw_data_array: CPURawDataArray, purpose: enums.CPUPurpose
):
"""Identifies a CPU with a given purpose, returning a :class:`CPUInfo` instance."""
c_cpu_id = ffi.new("struct cpu_id_t *")
if (
lib.cpu_request_core_type(
purpose, raw_data_array.c_cpu_raw_data_array, c_cpu_id
)
!= 0
):
raise CLibraryError
return CPUInfo.from_c(c_cpu_id)
@property
def architecture(self) -> enums.CPUArchitecture:
"""The CPU Architecture."""
try:
return enums.CPUArchitecture(self._c_cpu_id.architecture)
except ValueError:
return enums.CPUArchitecture.UNKNOWN
@property
def feature_level(self) -> enums.CPUFeatureLevel:
"""The CPU feature level."""
try:
return enums.CPUFeatureLevel(self._c_cpu_id.feature_level)
except ValueError:
return enums.CPUFeatureLevel.UNKNOWN
@property
def vendor_str(self) -> str:
"""The CPU vendor string, e.g., :const:`'GenuineIntel'`."""
return c_string_to_str(self._c_cpu_id.vendor_str)
@property
def brand_str(self) -> str:
"""The CPU brand string, e.g., :const:`'Intel(R) Xeon(TM) CPU 2.40GHz'`."""
return c_string_to_str(self._c_cpu_id.brand_str)
@property
def vendor(self) -> enums.CPUVendor:
"""The CPU vendor."""
try:
return enums.CPUVendor(self._c_cpu_id.vendor)
except ValueError:
return enums.CPUVendor.UNKNOWN
@property
def features(self) -> set[enums.CPUFeature]:
"""The set of supported CPU features."""
if self._features is None:
self._features = {
flag
for flag in enums.CPUFeature
if flag >= 0 and self._c_cpu_id.flags[flag]
}
return self._features
@property
def num_cores(self) -> int:
"""The number of CPU cores."""
return self._c_cpu_id.num_cores
@property
def num_logical_cpus(self) -> int:
"""The number of logical processors."""
return self._c_cpu_id.num_logical_cpus
@property
def total_logical_cpus(self) -> int:
"""The total number of logical processors."""
return self._c_cpu_id.total_logical_cpus
@property
def l1_data_cache(self) -> Optional[int]:
"""L1 data cache size in KB. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_data_cache)
@property
def l1_instruction_cache(self) -> Optional[int]:
"""L1 instruction cache size in KB. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_instruction_cache)
@property
def l2_cache(self) -> Optional[int]:
"""L2 cache size in KB. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l2_cache)
@property
def l3_cache(self) -> Optional[int]:
"""L3 cache size in KB. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l3_cache)
@property
def l4_cache(self) -> Optional[int]:
"""L4 cache size in KB. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l4_cache)
@property
def l1_data_assoc(self) -> Optional[int]:
"""L1 data cache associativity. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_data_assoc)
@property
def l1_instruction_assoc(self) -> Optional[int]:
"""L1 instruction cache associativity. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_instruction_assoc)
@property
def l2_assoc(self) -> Optional[int]:
"""L2 cache associativity. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l2_assoc)
@property
def l3_assoc(self) -> Optional[int]:
"""L3 cache associativity. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l3_assoc)
@property
def l4_assoc(self) -> Optional[int]:
"""L4 cache associativity. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l4_assoc)
@property
def l1_data_cacheline(self) -> Optional[int]:
"""L1 data cache line size. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_data_cacheline)
@property
def l1_instruction_cacheline(self) -> Optional[int]:
"""L1 instruction cache line size. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_instruction_cacheline)
@property
def l2_cacheline(self) -> Optional[int]:
"""L2 cache line size. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l2_cacheline)
@property
def l3_cacheline(self) -> Optional[int]:
"""L3 cache line size. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l3_cacheline)
@property
def l4_cacheline(self) -> Optional[int]:
"""L4 cache line size. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l4_cacheline)
@property
def l1_data_instances(self) -> Optional[int]:
"""Number of L1 data cache instances. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_data_instances)
@property
def l1_instruction_instances(self) -> Optional[int]:
"""Number of L1 instruction cache instances. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l1_instruction_instances)
@property
def l2_instances(self) -> Optional[int]:
"""Number of L2 cache instances. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l2_instances)
@property
def l3_instances(self) -> Optional[int]:
"""Number of L3 cache instances. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l3_instances)
@property
def l4_instances(self) -> Optional[int]:
"""Number of L4 cache instances. :const:`None` if not determined."""
return optional_int(self._c_cpu_id.l4_instances)
@property
def cpu_codename(self) -> str:
"""A human-friendly CPU codename."""
return c_string_to_str(self._c_cpu_id.cpu_codename)
@property
def detection_hints(self) -> set[enums.CPUHint]:
"""A set of miscellaneous detection hints."""
if self._detection_hints is None:
self._detection_hints = {
hint
for hint in enums.CPUHint
if hint >= 0 and self._c_cpu_id.detection_hints[hint]
}
return self._detection_hints
@property
def affinity_mask(self) -> bytes:
"""A bit mask of the affinity IDs that this processor type is occupying."""
bit_mask = getattr(self._c_cpu_id.affinity_mask, "__bits")
return b"".join(reversed([byte.to_bytes(1) for byte in bit_mask])).lstrip(
b"\x00"
)
@property
def purpose(self) -> enums.CPUPurpose:
"""The purpose of the CPU type, relevant for hybrid CPUs."""
return enums.CPUPurpose(self._c_cpu_id.purpose)
@property
def hypervisor(self) -> Optional[enums.HypervisorVendor]:
"""The hypervisor vendor or :const:`None` if not detected."""
hypervisor = lib.cpuid_get_hypervisor(ffi.NULL, self._c_cpu_id)
return enums.HypervisorVendor(hypervisor) if hypervisor != 0 else None
class X86Info(CPUInfo):
"""
The :class:`CPUInfo` child class for x86 CPUs.
"""
def __init__(self, c_cpu_id):
super().__init__(c_cpu_id)
self._c_cpu_id = c_cpu_id
@property
def family(self) -> int:
"""The CPU family number."""
return self._c_cpu_id.family
@property
def model(self) -> int:
"""The CPU model number."""
return self._c_cpu_id.model
@property
def stepping(self) -> int:
"""The CPU stepping."""
return self._c_cpu_id.stepping
@property
def ext_family(self) -> int:
"""The CPU display family number."""
return self._c_cpu_id.ext_family
@property
def ext_model(self) -> int:
"""The CPU display model number."""
return self._c_cpu_id.ext_model
@property
def sse_size(self) -> Optional[int]:
"""SSE execution unit size (64 or 128), :const:`None` if not available."""
return optional_int(self._c_cpu_id.sse_size)
@property
def sgx(self) -> Optional[SGX]:
"""SGX-related features if present, otherwise :const:`None`."""
return SGX(self._c_cpu_id.sgx) if self._c_cpu_id.sgx.present == 1 else None
class ARMInfo(CPUInfo):
"""
The :class:`CPUInfo` child class for ARM CPUs.
"""
def __init__(self, c_cpu_id):
super().__init__(c_cpu_id)
self._c_cpu_id = c_cpu_id
@property
def implementer(self) -> int:
"""The CPU implementer code."""
return self._c_cpu_id.implementer
@property
def variant(self) -> int:
"""The CPU variant number."""
return self._c_cpu_id.variant
@property
def part_num(self) -> int:
"""The CPU primary part number."""
return self._c_cpu_id.part_num
@property
def revision(self) -> int:
"""The CPU revision number."""
return self._c_cpu_id.revision
class SystemInfo:
"""
Class for holding structured :class:`CPUInfo` information about multiple CPUs.
Instances of this class can be indexed like lists, each item (of type :class:`CPUInfo`)
corresponds to a different :class:`CPUPurpose`.
"""
def __init__(self, c_system_id, cpu_info_list: list[CPUInfo]):
self._c_system_id = c_system_id
self._cpu_info_list = cpu_info_list
def __del__(self):
lib.cpuid_free_system_id(self._c_system_id)
def __getitem__(self, index: int) -> CPUInfo:
return self._cpu_info_list[index]
def __len__(self) -> int:
return len(self._cpu_info_list)
@classmethod
def from_c(cls, c_system_id):
"""Create a :class:`SystemInfo` instance from the corresponding C structure."""
cpu_id_list = []
for c_cpu_id in c_system_id.cpu_types[0 : c_system_id.num_cpu_types]:
new_c_cpu_id = ffi.new("struct cpu_id_t *")
ffi.memmove(
new_c_cpu_id, ffi.addressof(c_cpu_id), ffi.sizeof("struct cpu_id_t")
)
cpu_id_list.append(CPUInfo.from_c(new_c_cpu_id))
return cls(c_system_id, cpu_id_list)
@classmethod
def from_all_cpus(cls):
"""Create a :class:`SystemInfo` instance by indentifying all CPUs."""
c_system_id = ffi.new("struct system_id_t *")
if lib.cpu_identify_all(ffi.NULL, c_system_id) != 0:
raise CLibraryError
return cls.from_c(c_system_id)
@classmethod
def from_raw_array(cls, raw_data_array: CPURawDataArray):
"""Create a :class:`SystemInfo` instance from an instance of :class:`CPURawDataArray`"""
c_system_id = ffi.new("struct system_id_t *")
if lib.cpu_identify_all(raw_data_array.c_cpu_raw_data_array, c_system_id) != 0:
raise CLibraryError
return SystemInfo.from_c(c_system_id)
@property
def l1_data_total_instances(self) -> Optional[int]:
"""The number of total L1 data cache instances. :const:`None` if not determined."""
return optional_int(self._c_system_id.l1_data_total_instances)
@property
def l1_instruction_total_instances(self) -> Optional[int]:
"""The number of total L1 instruction cache instances. :const:`None` if not determined."""
return optional_int(self._c_system_id.l1_instruction_total_instances)
@property
def l2_total_instances(self) -> Optional[int]:
"""The number of total L2 cache instances. :const:`None` if not undetermined"""
return optional_int(self._c_system_id.l2_total_instances)
@property
def l3_total_instances(self) -> Optional[int]:
"""The number of total L3 cache instances. :const:`None` if not undetermined"""
return optional_int(self._c_system_id.l3_total_instances)
@property
def l4_total_instances(self) -> Optional[int]:
"""The number of total L4 cache instances. :const:`None` if not undetermined"""
return optional_int(self._c_system_id.l4_total_instances)

View file

@ -0,0 +1,63 @@
"""
Module providing access to information from model-specific registers.
"""
from typing import Optional
from libcpuid.errors import CLibraryError
from libcpuid._utils import get_enum_options
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
ffi,
)
class MSR:
"""
Class for reading information from model-specific registers (MSR).
Since `rdmsr` is a privileged instruction, you need ring 0 access to
instantiate this class.
To see the documentation of properties, look for :const:`cpu_msrinfo_request_t`
in :const:`libcpuid.h`, the main header file of the underlying C library.
"""
def __init__(self, core: int = 0):
self._handle = lib.cpu_msr_driver_open_core(core)
if self._handle == ffi.NULL:
raise CLibraryError
def __del__(self):
if lib.cpu_msr_driver_close(self._handle) != 0:
raise CLibraryError
def serialize_raw_data(self, filename: str):
"""Writes raw MSR data to a file."""
if lib.msr_serialize_raw_data(self._handle, filename.encode()) != 0:
raise CLibraryError
def read_register(self, msr_index: int) -> bytes:
"""Returns the content of a model-specific register with the given index."""
result = ffi.new("uint64_t *")
if lib.cpu_rdmsr(self._handle, msr_index, result) != 0:
raise CLibraryError
return result[0].to_bytes(8)
def _get_msrinfo_request(enum_value):
def _msrinfo_request(self) -> Optional[int]:
"""May return :const:`None` if not available."""
result = lib.cpu_msrinfo(
self._handle, enum_value # pylint: disable=protected-access
)
return None if result == 0x3FFFFFFF else result
return _msrinfo_request
# This adds properties to the MSR class, copying the possible
# inputs to the cpu_msrinfo function of the C library, such as
# mperf, aperf, min_multiplier, temperature, or voltage.
# See cpu_msr_info_request_t in libcpuid.h for details.
for name, value in get_enum_options("INFO_", ignore=["BCLK"]).items():
setattr(MSR, name.lower(), property(_get_msrinfo_request(value)))

105
python/src/libcpuid/raw.py Normal file
View file

@ -0,0 +1,105 @@
"""
Module providing access to raw CPU data.
"""
from libcpuid.errors import CLibraryError
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
ffi,
)
class CPURawData:
"""
Class holding raw data about a single logical CPU.
"""
def __init__(self, c_cpu_raw_data):
self._c_cpu_raw_data = c_cpu_raw_data
@property
def c_cpu_raw_data(self):
"""Returns the underlying C structure."""
return self._c_cpu_raw_data
def serialize(self, filename: str):
"""Exports the raw data into a provided file."""
lib.cpuid_serialize_raw_data(self._c_cpu_raw_data, filename.encode())
@classmethod
def from_current_cpu(cls):
"""Creates a :class:`CPURawData` instance by running `cpuid` on the current CPU."""
c_cpu_raw_data = ffi.new("struct cpu_raw_data_t *")
if lib.cpuid_get_raw_data(c_cpu_raw_data) != 0:
raise CLibraryError
return cls(c_cpu_raw_data)
@classmethod
def from_cpu_core(cls, logical_cpu: int):
"""Creates a :class:`CPURawData` instance by running `cpuid` on the specified CPU."""
c_cpu_raw_data = ffi.new("struct cpu_raw_data_t *")
if lib.cpuid_get_raw_data_core(c_cpu_raw_data, logical_cpu) != 0:
raise CLibraryError
return cls(c_cpu_raw_data)
@classmethod
def from_file(cls, filename: str):
"""Creates a :class:`CPURawData` instance by parsing the data from a provided file."""
c_cpu_raw_data = ffi.new("struct cpu_raw_data_t *")
if lib.cpuid_deserialize_raw_data(c_cpu_raw_data, filename.encode()) != 0:
raise CLibraryError
return cls(c_cpu_raw_data)
class CPURawDataArray:
"""
Class holding raw data about multiple CPUs.
Instances of this class can be indexed like lists. each item
(of type :class:`CPURawData`) holds data about one **logical** CPU.
"""
def __init__(self, c_cpu_raw_data_array):
self._c_cpu_raw_data_array = c_cpu_raw_data_array
self._cpu_raw_data_list = [
CPURawData(c_cpu_raw_data)
for c_cpu_raw_data in self._c_cpu_raw_data_array.raw[
0 : self._c_cpu_raw_data_array.num_raw
]
]
def __del__(self):
lib.cpuid_free_raw_data_array(self._c_cpu_raw_data_array)
def __getitem__(self, index: int) -> CPURawData:
return self._cpu_raw_data_list[index]
def __len__(self) -> int:
return len(self._cpu_raw_data_list)
@property
def c_cpu_raw_data_array(self):
"""Returns the underlying C structure."""
return self._c_cpu_raw_data_array
def serialize(self, filename: str):
"""Exports the raw data array into a provided file."""
lib.cpuid_serialize_all_raw_data(self._c_cpu_raw_data_array, filename.encode())
@classmethod
def from_all_cpus(cls):
"""Creates a :class:`CPURawDataArray` instance by running `cpuid` on all CPUs."""
c_cpu_raw_data_array = ffi.new("struct cpu_raw_data_array_t *")
if lib.cpuid_get_all_raw_data(c_cpu_raw_data_array) != 0:
raise CLibraryError
return cls(c_cpu_raw_data_array)
@classmethod
def from_file(cls, filename: str):
"""Creates a :class:`CPURawDataArray` instance by parsing the data from a provided file."""
c_cpu_raw_data_array = ffi.new("struct cpu_raw_data_array_t *")
if (
lib.cpuid_deserialize_all_raw_data(c_cpu_raw_data_array, filename.encode())
!= 0
):
raise CLibraryError
return cls(c_cpu_raw_data_array)

View file

@ -0,0 +1,96 @@
"""
Module dealing with SGX-related CPU information.
"""
from libcpuid import enums
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
lib,
ffi,
)
class EPC:
"""
Class holding information about a single EPC (Enclave Page Cache) area.
"""
def __init__(self, c_epc):
self._c_epc = c_epc
@property
def start_addr(self) -> int:
"""Start address."""
return self._c_epc.start_addr
@property
def length(self) -> int:
"""Length of the area."""
return self._c_epc.length
class SGX:
"""
Class holding the SGX-related information about a CPU.
"""
def __init__(self, c_sgx):
self._c_sgx = c_sgx
self._features = None
@property
def max_enclave_32bit(self) -> int:
"""The maximum enclave size in 32-bit mode."""
return self._c_sgx.max_enclave_32bit
@property
def max_enclave_64bit(self) -> int:
"""The maximum enclave size in 64-bit mode."""
return self._c_sgx.max_enclave_64bit
@property
def features(self) -> set[enums.SGXFeature]:
"""Set of CPU SGX features."""
if self._features is None:
self._features = {
flag
for flag in enums.SGXFeature
if flag >= 0 and self._c_sgx.flags[flag]
}
return self._features
@property
def num_epc_sections(self) -> int:
"""The number of Enclave Page Cache (EPC) sections."""
return self._c_sgx.num_epc_sections
@property
def misc_select(self) -> bytes:
"""
Bit vector of the supported extended features that can be written
to the MISC region of the SSA (Save State Area)
"""
return self._c_sgx.misc_select.to_bytes(4)
@property
def secs_attributes(self) -> bytes:
"""
Bit vector of the attributes that can be set to SECS.ATTRIBUTES
via ECREATE.
"""
return self._c_sgx.secs_attributes.to_bytes(8)
@property
def secs_xfrm(self) -> bytes:
"""
Bit vector of the attributes that can be set in the XSAVE feature
request mask.
"""
return self._c_sgx.secs_xfrm.to_bytes(8)
def get_epc(self, index: int) -> EPC:
"""Fetches information about a single EPC area given by `index`."""
if not 0 <= index < self.num_epc_sections:
raise IndexError(
f"Invalid index {index}, the CPU has {self.num_epc_sections} EPC sections."
)
return EPC(lib.cpuid_get_epc(index, ffi.NULL))