Skip to content

Commit

Permalink
Swap ctypes for cffi
Browse files Browse the repository at this point in the history
  • Loading branch information
zacikpa committed Jul 1, 2024
1 parent d6a429b commit da0de29
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 544 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ jobs:
with:
python-version: '3.12'

- name: Check code formatting (black)
run: |
pip install black
black --check python
- name: Perform static analysis (pylint)
run: |
pip install pylint
pylint python/src/libcpuid
- name: Build and install libcpuid
run: |
sudo apt-get install autoconf libtool automake
Expand All @@ -39,9 +29,19 @@ jobs:
sudo make install
sudo ldconfig
- name: Check code formatting (black)
run: |
pip install black
black --check python
- name: Install the libcpuid Python package
run: pip install ./python

- name: Perform static analysis (pylint)
run: |
pip install pylint
pylint python/src/libcpuid
- name: Run tests
run: |
pip install pytest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ build
*__pycache__
python/docs/_build
python/dist
python/src/libcpuid/_libcpuid_cffi.*
python/src/libcpuid/libcpuid.h
*.egg-info
14 changes: 14 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ build:
os: ubuntu-22.04
tools:
python: latest
apt_packages:
- autoconf
- libtool
- automake
jobs:
pre_install:
- libtoolize
- autoreconf --install
- mkdir ./install
- ./configure --prefix=`pwd`/install
- make
- make install
- pip install cffi
- python ./python/ffi_build_rtd.py ./libcpuid/libcpuid.h ./install

sphinx:
configuration: python/docs/conf.py
Expand Down
5 changes: 0 additions & 5 deletions python/docs/api/cpuid.rst

This file was deleted.

8 changes: 8 additions & 0 deletions python/docs/api/info.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Classes storing CPU information
===============================

.. autoclass:: libcpuid.cpuid.CPUID
:members:

.. autoclass:: libcpuid.cpusgx.CPUSGX
:members:
37 changes: 19 additions & 18 deletions python/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,37 @@ to the libcpuid C library.
.. code-block:: python
import sys
import libcpuid
# Initialize the libcpuid library
try:
lib = libcpuid.LibCPUID()
except libcpuid.LibCPUIDError as err:
print(err)
sys.exit(1)
from libcpuid import [
libcpuid_version,
get_total_cpus,
cpuid_present,
cpu_identify,
get_cpu_list,
LibCPUIDError
]
from libcpuid.enums import CPUFeature, CPUVendor
# print the version of the libcpuid library
print(lib.version())
print(libcpuid_version())
# print the number of CPU cores
print(lib.get_total_cpus())
print(get_total_cpus())
# check if the cpuid instruction is available
if not lib.cpuid_present():
if not cpuid_present():
print("CPUID instruction is not available")
sys.exit(1)
try:
# run the main CPU identification function
# and print information about the CPU
cpuid = lib.cpu_identify()
print(cpuid.vendor.name)
print(cpuid.architecture.name)
print(cpuid.has_feature(libcpuid.enums.CPUFeature.FPU))
id = cpu_identify()
print(id.vendor.name)
print(id.architecture.name)
print(CPUFeature.FPU in id.flags)
# print the list of all Intel CPU code names
print(lib.get_cpu_list(libcpuid.enums.CPUVendor.INTEL))
except libcpuid.LibCPUIDError as err:
print(get_cpu_list(CPUVendor.INTEL))
except LibCPUIDError as err:
print(err)
sys.exit(1)
Expand All @@ -46,5 +47,5 @@ to the libcpuid C library.

Home <self>
api/libcpuid
api/cpuid
api/info
api/enums
72 changes: 72 additions & 0 deletions python/ffi_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Module for compiling the C FFI.
"""

import subprocess
import os
from cffi import FFI


class FFIBuildException(Exception):
"""Generic exception for errors occuring during the CFFI build."""


def find_header():
"""
Obtains libcpuid header file location via pkg-config.
"""
try:
cflags = (
subprocess.check_output(["pkg-config", "libcpuid", "--cflags-only-I"])
.decode()
.strip()
.split()
)
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

# Find an existing libcpuid header file
header_path = None # pylint: disable=invalid-name
for cflag in cflags:
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).
"""
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


header = find_header()

ffibuilder = FFI()
ffibuilder.cdef(preprocess_header(header))
ffibuilder.set_source_pkgconfig(
"libcpuid._libcpuid_cffi", ["libcpuid"], "#include <libcpuid.h>"
)
26 changes: 26 additions & 0 deletions python/ffi_build_rtd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Script for compiling the C FFI for the live documentation.
"""

import subprocess
import sys
import os
from cffi import FFI

if __name__ == "__main__":
header = sys.argv[1]
install_dir = sys.argv[2]
library_dir = os.path.join(os.getcwd(), install_dir, "lib")
ffibuilder = FFI()
ffibuilder.cdef(
subprocess.check_output(["gcc", "-U __GNUC__", "-E", header]).decode()
)
ffibuilder.set_source(
"python.src.libcpuid._libcpuid_cffi",
"#include <libcpuid.h>",
libraries=["cpuid"],
library_dirs=[library_dir],
include_dirs=[os.path.join(install_dir, "include", "libcpuid")],
extra_link_args=[f"-Wl,-rpath={library_dir}"],
)
ffibuilder.compile(verbose=True)
5 changes: 3 additions & 2 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
[build-system]
requires = ["setuptools"]
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 = "[email protected]"}]
description = "Python bindings for the libcpuid C library"
description = "Python bindings for the libcpuid C library"
5 changes: 5 additions & 0 deletions python/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from setuptools import setup

setup(
cffi_modules=["ffi_build.py:ffibuilder"],
)
113 changes: 34 additions & 79 deletions python/src/libcpuid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,53 @@
libcpuid C library, which provides CPU identification.
"""

from sys import platform
from ctypes import CDLL, c_bool, c_char_p, POINTER, byref
from . import enums, types, cpuid
from libcpuid._libcpuid_cffi import ( # pylint: disable=no-name-in-module, import-error
ffi,
lib,
)
from libcpuid import cpuid, enums
from libcpuid._utils import c_string_to_str


class LibCPUIDError(Exception):
"""Raised when an error occurs in the libcpuid library."""


class LibCPUID:
"""The main class wrapping the libcpuid C library."""
def _check_error(return_code: int) -> None:
"""Translates return codes to exceptions."""
if return_code != 0:
raise LibCPUIDError(c_string_to_str(lib.cpuid_error()))

def __init__(self):
if platform.startswith("win32") or platform.startswith("cygwin"):
dll_name = "libcpuid.dll"
elif platform.startswith("linux"):
dll_name = "libcpuid.so"
else:
raise LibCPUIDError(f"Unsupported platform: {platform}")
try:
self._dll = CDLL(dll_name)
self._set_function_prototypes()
except OSError as ex:
raise LibCPUIDError("Unable to load the libcpuid library") from ex

def _check_error(self, return_code: int) -> None:
"""Translates return codes to exceptions."""
if return_code != 0:
raise LibCPUIDError(self._dll.cpuid_error().decode())
def libcpuid_version() -> str:
"""Returns the version of the libcpuid library."""
return c_string_to_str(lib.cpuid_lib_version())

def _set_function_prototypes(self):
"""Sets the C function prototypes according to the libcpuid API."""
# char *cpuid_lib_version(void);
self._dll.cpuid_lib_version.argtypes = []
self._dll.cpuid_lib_version.restype = c_char_p

# bool cpuid_present(void)
self._dll.cpuid_present.argtypes = []
self._dll.cpuid_present.restype = c_bool
def cpuid_present() -> bool:
"""Checks if the cpuid instruction is supported."""
return bool(lib.cpuid_present())

# int cpu_identify(struct cpu_raw_data_t* raw, struct cpu_id_t* data)
self._dll.cpu_identify.argtypes = [
POINTER(types.CPURawData),
POINTER(types.CPUID),
]
self._dll.cpu_identify.restype = self._check_error

# void cpuid_get_cpu_list(cpu_vendor_t vendor, struct cpu_list_t* list)
self._dll.cpuid_get_cpu_list.argtypes = [
types.CPUVendor,
POINTER(types.CPUList),
]
self._dll.cpuid_get_cpu_list.restype = None
def get_total_cpus() -> int:
"""Returns the total number of logical CPU threads."""
return lib.cpuid_get_total_cpus()

# void cpuid_free_cpu_list(struct cpu_list_t* list);
self._dll.cpuid_free_cpu_list.argtypes = [POINTER(types.CPUList)]
self._dll.cpuid_free_cpu_list.restype = None

# const char* cpuid_error(void);
self._dll.cpuid_error.argtypes = []
self._dll.cpuid_error.restype = c_char_p
def cpu_identify() -> cpuid.CPUID:
"""Identifies the CPU and returns a :class:`cpuid.CPUID` instance."""
raw_cpu_id = ffi.new("struct cpu_id_t *")
_check_error(lib.cpu_identify(ffi.NULL, raw_cpu_id))
return cpuid.CPUID(raw_cpu_id)

# hypervisor_vendor_t
# cpuid_get_hypervisor(struct cpu_raw_data_t* raw, struct cpu_id_t* data)
self._dll.cpuid_get_hypervisor.argtypes = [
POINTER(types.CPURawData),
POINTER(types.CPUID),
]
self._dll.cpuid_get_hypervisor.restype = types.HypervisorVendor

def version(self) -> str:
"""Returns the version of the libcpuid library."""
return self._dll.cpuid_lib_version().decode()

def cpuid_present(self) -> bool:
"""Checks if the cpuid instruction is supported."""
return self._dll.cpuid_present()

def get_total_cpus(self) -> int:
"""Returns the total number of logical CPU threads."""
return self._dll.cpuid_get_total_cpus()

def cpu_identify(self) -> cpuid.CPUID:
"""Identifies the CPU and returns a :class:`cpuid.CPUID` instance."""
cpu_id = types.CPUID()
self._dll.cpu_identify(None, byref(cpu_id))
return cpuid.CPUID(cpu_id, self._dll)

def get_cpu_list(self, vendor: enums.CPUVendor) -> list[str]:
"""Gets a list of CPU :meth:`codenames <cpuid.CPUID.cpu_codename>` for a specific vendor."""
cpu_list = types.CPUList()
self._dll.cpuid_get_cpu_list(vendor, byref(cpu_list))
if cpu_list.num_entries == 0:
raise LibCPUIDError(self._dll.cpuid_error().decode())
return [name.decode() for name in cpu_list.names[: cpu_list.num_entries]]
def get_cpu_list(vendor: enums.CPUVendor) -> list[str]:
"""Gets a list of CPU :meth:`codenames <cpuid.CPUID.cpu_codename>` for a specific vendor."""
raw_cpu_list = ffi.new("struct cpu_list_t *")
lib.cpuid_get_cpu_list(vendor, raw_cpu_list)
if raw_cpu_list.num_entries == 0:
raise LibCPUIDError(c_string_to_str(lib.cpuid_error()))
return [
c_string_to_str(name)
for name in raw_cpu_list.names[0 : raw_cpu_list.num_entries]
]
Loading

0 comments on commit da0de29

Please sign in to comment.