commit fa82965ab52e12cde6db41bd8a7a807eff9d0980 Author: ImSavsis Date: Sun Jul 19 23:29:46 2026 +0500 init: raw cpuid/rdtsc access via ctypes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..497e752 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + push: + branches: [master] + +jobs: + test: + runs-on: windows-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: ilammy/msvc-dev-cmd@v1 + - run: cl /LD /EHsc /Fe:src\rawcpu\rawcpu.dll native\rawcpu.cpp + - run: pip install -e .[dev] + - run: pytest --cov=rawcpu --cov-report=xml + - uses: codecov/codecov-action@v4 + if: matrix.python-version == '3.12' + with: + files: ./coverage.xml + - run: python -m build + - run: python -m twine check dist/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4a6baa6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,23 @@ +name: publish + +on: + release: + types: [published] + +permissions: + id-token: write + +jobs: + publish: + runs-on: windows-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: ilammy/msvc-dev-cmd@v1 + - run: cl /LD /EHsc /Fe:src\rawcpu\rawcpu.dll native\rawcpu.cpp + - run: pip install build + - run: python -m build + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..312c8f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +.pytest_cache/ +dist/ +build/ +*.egg-info/ +.claude/ +.coverage +coverage.xml +*.obj +*.exp +*.lib +src/rawcpu/rawcpu.dll diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5313c5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Savsis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a70dcd9 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# rawcpu + +[![CI](https://github.com/ImSavsis/rawcpu/actions/workflows/ci.yml/badge.svg)](https://github.com/ImSavsis/rawcpu/actions/workflows/ci.yml) +[![Coverage](https://codecov.io/gh/ImSavsis/rawcpu/branch/master/graph/badge.svg)](https://codecov.io/gh/ImSavsis/rawcpu) +[![pypi](https://img.shields.io/pypi/v/rawcpu.svg)](https://pypi.org/project/rawcpu/) +[![downloads](https://img.shields.io/pypi/dm/rawcpu.svg)](https://pypi.org/project/rawcpu/) +[![versions](https://img.shields.io/pypi/pyversions/rawcpu.svg)](https://pypi.org/project/rawcpu/) +[![license](https://img.shields.io/github/license/ImSavsis/rawcpu.svg)](https://github.com/ImSavsis/rawcpu/blob/master/LICENSE) + +прямой доступ к инструкциям `CPUID`/`RDTSC` из питона — то, что нормально делает только C++, а чистый питон вообще никак. крошечная нативная dll под капотом + ctypes сверху, никакого Python C API. + +```mermaid +sequenceDiagram + App->>rawcpu: cpuid(1) / rdtsc() + rawcpu->>DLL: ctypes call + DLL->>CPU: инструкция CPUID/RDTSC напрямую + CPU-->>DLL: сырые регистры / счётчик тактов + DLL-->>App: tuple(int) / int +``` + +## установка + +``` +pip install rawcpu +``` + +пока только windows x64. + +## юзать + +```python +import rawcpu + +print(rawcpu.vendor()) # 'GenuineIntel' / 'AuthenticAMD' +print(rawcpu.brand()) # 'AMD Ryzen 9 ...' +print(rawcpu.has_feature("avx2")) # True/False, без запуска кода который может упасть на старом CPU + +start = rawcpu.rdtsc() +# твой код +end = rawcpu.rdtsc() +print(end - start, "тактов") # точнее чем time.perf_counter(), считает реальные такты CPU +``` + +## документация + +- [quickstart](docs/quickstart.md) +- [api](docs/api.md) + +## фичи + +- сырой `cpuid(leaf, subleaf)` — регистры eax/ebx/ecx/edx как есть +- `rdtsc()` — счётчик тактов CPU напрямую, без ОС-абстракции +- `has_feature()` — проверка sse/avx/avx2/aes/fma и т.д. по битам без падений на неподдерживающем CPU +- typed, питон 3.10–3.13 + +## чего нет + +- linux/mac — только windows пока +- запись в регистры / что-то в духе MSR — это уже ring 0, юзермодом не достать без драйвера diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..e95c90a --- /dev/null +++ b/docs/api.md @@ -0,0 +1,25 @@ +# api + +## `cpuid(leaf: int, subleaf: int = 0) -> tuple[int, int, int, int]` + +сырой вызов инструкции `CPUID`, возвращает `(eax, ebx, ecx, edx)`. + +## `rdtsc() -> int` + +сырое значение счётчика тактов процессора (инструкция `RDTSC`), монотонно растёт. + +## `vendor() -> str` + +строка вендора CPU, например `GenuineIntel` или `AuthenticAMD`. + +## `brand() -> str` + +полная строка модели процессора из расширенных leaf'ов CPUID. + +## `has_feature(name: str) -> bool` + +проверка бита фичи в CPUID без риска упасть на CPU без её поддержки. кидает `ValueError` если имя не из `FEATURES`. + +## `FEATURES: dict[str, tuple[int, str, int]]` + +словарь `имя -> (leaf, регистр, номер бита)`. поддерживаются: `sse`, `sse2`, `sse3`, `ssse3`, `fma`, `sse4_1`, `sse4_2`, `popcnt`, `aes`, `avx`, `rdrnd`, `avx2`, `avx512f`. diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..c55e1b7 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,48 @@ +# quickstart + +## установка + +``` +pip install rawcpu +``` + +## узнать процессор + +```python +import rawcpu + +print(rawcpu.vendor()) # GenuineIntel / AuthenticAMD +print(rawcpu.brand()) # полная строка модели +``` + +## проверить фичи перед SIMD-кодом + +```python +if rawcpu.has_feature("avx2"): + # можно грузить AVX2-путь + ... +else: + # fallback + ... +``` + +список доступных имён — `rawcpu.FEATURES`. + +## точный замер тактов + +`time.perf_counter()` меряет время через ОС, `rdtsc()` — реальные такты процессора напрямую: + +```python +start = rawcpu.rdtsc() +do_something() +end = rawcpu.rdtsc() +print(end - start, "cycles") +``` + +## сырой cpuid + +```python +eax, ebx, ecx, edx = rawcpu.cpuid(leaf=7, subleaf=0) +``` + +leaf/subleaf — как в интеловской документации по CPUID. diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..2c5f972 --- /dev/null +++ b/llms.txt @@ -0,0 +1,18 @@ +# rawcpu + +> Raw CPUID and RDTSC access for Python via a tiny native DLL wrapped with ctypes. Exposes CPU feature detection (SSE/AVX/AES/etc.) and cycle-accurate timing that plain Python cannot do natively. + +Windows x64 only for now. No Python C API used — a small MSVC-compiled DLL exports two functions (raw_cpuid, raw_rdtsc) loaded via ctypes.WinDLL, so it works across Python 3.10-3.13 without per-version ABI builds. + +## Docs + +- [README](https://github.com/ImSavsis/rawcpu/blob/master/README.md): install, usage, feature list +- [docs/quickstart.md](https://github.com/ImSavsis/rawcpu/blob/master/docs/quickstart.md) +- [docs/api.md](https://github.com/ImSavsis/rawcpu/blob/master/docs/api.md) +- [native/rawcpu.cpp](https://github.com/ImSavsis/rawcpu/blob/master/native/rawcpu.cpp): the native DLL source +- [src/rawcpu/__init__.py](https://github.com/ImSavsis/rawcpu/blob/master/src/rawcpu/__init__.py): high-level API and CPU feature bit table + +## Notes + +- No MSR/register-write access — that requires ring 0, not reachable from usermode without a kernel driver +- Linux/macOS not supported yet diff --git a/native/rawcpu.cpp b/native/rawcpu.cpp new file mode 100644 index 0000000..1e62688 --- /dev/null +++ b/native/rawcpu.cpp @@ -0,0 +1,18 @@ +#include + +extern "C" { + +__declspec(dllexport) void raw_cpuid(int leaf, int subleaf, unsigned int* out) { + int regs[4]; + __cpuidex(regs, leaf, subleaf); + out[0] = static_cast(regs[0]); + out[1] = static_cast(regs[1]); + out[2] = static_cast(regs[2]); + out[3] = static_cast(regs[3]); +} + +__declspec(dllexport) unsigned long long raw_rdtsc() { + return __rdtsc(); +} + +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f5b1aad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "rawcpu" +version = "0.1.0" +description = "raw CPUID and RDTSC access for Python via a tiny native DLL — CPU feature detection and cycle-accurate timing" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [{ name = "Savsis" }] +keywords = ["cpuid", "rdtsc", "cpu", "low-level", "simd", "intrinsics"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", +] + +[project.urls] +Homepage = "https://github.com/ImSavsis/rawcpu" +Issues = "https://github.com/ImSavsis/rawcpu/issues" + +[project.optional-dependencies] +dev = ["pytest", "pytest-cov", "build", "twine"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +rawcpu = ["*.dll", "py.typed"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f8cc35d --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +from setuptools import setup +from setuptools.dist import Distribution + +try: + from wheel.bdist_wheel import bdist_wheel as _bdist_wheel + + class bdist_wheel(_bdist_wheel): + def finalize_options(self): + _bdist_wheel.finalize_options(self) + self.root_is_pure = False + + def get_tag(self): + _python, _abi, plat = _bdist_wheel.get_tag(self) + return "py3", "none", plat + +except ImportError: + bdist_wheel = None + + +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True + + +setup( + distclass=BinaryDistribution, + cmdclass={"bdist_wheel": bdist_wheel} if bdist_wheel else {}, +) diff --git a/src/rawcpu/__init__.py b/src/rawcpu/__init__.py new file mode 100644 index 0000000..11f72c0 --- /dev/null +++ b/src/rawcpu/__init__.py @@ -0,0 +1,69 @@ +from ._native import cpuid_raw, rdtsc_raw + +__version__ = "0.1.0" + + +def cpuid(leaf: int, subleaf: int = 0): + return cpuid_raw(leaf, subleaf) + + +def rdtsc() -> int: + return rdtsc_raw() + + +def _regs_to_str(*regs: int) -> str: + out = bytearray() + for r in regs: + out += r.to_bytes(4, "little") + return out.decode("ascii", errors="replace") + + +def vendor() -> str: + _eax, ebx, ecx, edx = cpuid(0) + return _regs_to_str(ebx, edx, ecx) + + +def brand() -> str: + parts = [] + for leaf in (0x80000002, 0x80000003, 0x80000004): + eax, ebx, ecx, edx = cpuid(leaf) + parts.append(_regs_to_str(eax, ebx, ecx, edx)) + return "".join(parts).strip("\x00").strip() + + +FEATURES = { + "sse": (1, "edx", 25), + "sse2": (1, "edx", 26), + "sse3": (1, "ecx", 0), + "ssse3": (1, "ecx", 9), + "fma": (1, "ecx", 12), + "sse4_1": (1, "ecx", 19), + "sse4_2": (1, "ecx", 20), + "popcnt": (1, "ecx", 23), + "aes": (1, "ecx", 25), + "avx": (1, "ecx", 28), + "rdrnd": (1, "ecx", 30), + "avx2": (7, "ebx", 5), + "avx512f": (7, "ebx", 16), +} + +_REG_INDEX = {"eax": 0, "ebx": 1, "ecx": 2, "edx": 3} + + +def has_feature(name: str) -> bool: + if name not in FEATURES: + raise ValueError(f"unknown feature {name!r}, see rawcpu.FEATURES") + leaf, reg, bit = FEATURES[name] + regs = cpuid(leaf) + value = regs[_REG_INDEX[reg]] + return bool(value & (1 << bit)) + + +__all__ = [ + "cpuid", + "rdtsc", + "vendor", + "brand", + "has_feature", + "FEATURES", +] diff --git a/src/rawcpu/_native.py b/src/rawcpu/_native.py new file mode 100644 index 0000000..9ed8675 --- /dev/null +++ b/src/rawcpu/_native.py @@ -0,0 +1,21 @@ +import ctypes +import os + +_dll_path = os.path.join(os.path.dirname(__file__), "rawcpu.dll") +_lib = ctypes.WinDLL(_dll_path) + +_lib.raw_cpuid.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_uint32)] +_lib.raw_cpuid.restype = None + +_lib.raw_rdtsc.argtypes = [] +_lib.raw_rdtsc.restype = ctypes.c_uint64 + + +def cpuid_raw(leaf: int, subleaf: int = 0): + out = (ctypes.c_uint32 * 4)() + _lib.raw_cpuid(leaf, subleaf, out) + return tuple(out) + + +def rdtsc_raw() -> int: + return _lib.raw_rdtsc() diff --git a/src/rawcpu/py.typed b/src/rawcpu/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_rawcpu.py b/tests/test_rawcpu.py new file mode 100644 index 0000000..f326e2d --- /dev/null +++ b/tests/test_rawcpu.py @@ -0,0 +1,32 @@ +import rawcpu + + +def test_vendor_not_empty(): + assert len(rawcpu.vendor()) > 0 + + +def test_brand_not_empty(): + assert len(rawcpu.brand()) > 0 + + +def test_rdtsc_moves_forward(): + a = rawcpu.rdtsc() + b = rawcpu.rdtsc() + assert b >= a + + +def test_cpuid_leaf_zero_reports_max_leaf(): + eax, _ebx, _ecx, _edx = rawcpu.cpuid(0) + assert eax >= 1 + + +def test_has_feature_sse2_true_on_any_modern_x86(): + assert rawcpu.has_feature("sse2") is True + + +def test_has_feature_unknown_raises(): + try: + rawcpu.has_feature("not-a-real-feature") + assert False, "expected ValueError" + except ValueError: + pass