├── .github └── workflows │ ├── cibuildwheel.yml │ ├── publish_pypi.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin └── push-tag.sh ├── cymem ├── __init__.pxd ├── __init__.py ├── about.py ├── cymem.pxd ├── cymem.pyx └── tests │ ├── __init__.py │ └── test_import.py ├── pyproject.toml ├── requirements.txt └── setup.py /.github/workflows/cibuildwheel.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | # ytf did they invent their own syntax that's almost regex? 7 | # ** matches 'zero or more of any character' 8 | - 'release-v[0-9]+.[0-9]+.[0-9]+**' 9 | - 'prerelease-v[0-9]+.[0-9]+.[0-9]+**' 10 | jobs: 11 | build_wheels: 12 | name: Build wheels on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | # macos-13 is an intel runner, macos-14 is apple silicon 17 | os: [ubuntu-latest, windows-latest, macos-13, macos-14, ubuntu-24.04-arm] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Build wheels 22 | uses: pypa/cibuildwheel@v2.21.3 23 | env: 24 | CIBW_SOME_OPTION: value 25 | with: 26 | package-dir: . 27 | output-dir: wheelhouse 28 | config-file: "{package}/pyproject.toml" 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 32 | path: ./wheelhouse/*.whl 33 | 34 | build_sdist: 35 | name: Build source distribution 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Build sdist 41 | run: pipx run build --sdist 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: cibw-sdist 45 | path: dist/*.tar.gz 46 | create_release: 47 | needs: [build_wheels, build_sdist] 48 | runs-on: ubuntu-latest 49 | permissions: 50 | contents: write 51 | checks: write 52 | actions: read 53 | issues: read 54 | packages: write 55 | pull-requests: read 56 | repository-projects: read 57 | statuses: read 58 | steps: 59 | - name: Get the tag name and determine if it's a prerelease 60 | id: get_tag_info 61 | run: | 62 | FULL_TAG=${GITHUB_REF#refs/tags/} 63 | if [[ $FULL_TAG == release-* ]]; then 64 | TAG_NAME=${FULL_TAG#release-} 65 | IS_PRERELEASE=false 66 | elif [[ $FULL_TAG == prerelease-* ]]; then 67 | TAG_NAME=${FULL_TAG#prerelease-} 68 | IS_PRERELEASE=true 69 | else 70 | echo "Tag does not match expected patterns" >&2 71 | exit 1 72 | fi 73 | echo "FULL_TAG=$TAG_NAME" >> $GITHUB_ENV 74 | echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV 75 | echo "IS_PRERELEASE=$IS_PRERELEASE" >> $GITHUB_ENV 76 | - uses: actions/download-artifact@v4 77 | with: 78 | # unpacks all CIBW artifacts into dist/ 79 | pattern: cibw-* 80 | path: dist 81 | merge-multiple: true 82 | - name: Create Draft Release 83 | id: create_release 84 | uses: softprops/action-gh-release@v2 85 | if: startsWith(github.ref, 'refs/tags/') 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | with: 89 | name: ${{ env.TAG_NAME }} 90 | draft: true 91 | prerelease: ${{ env.IS_PRERELEASE }} 92 | files: "./dist/*" 93 | -------------------------------------------------------------------------------- /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | # The cibuildwheel action triggers on creation of a release, this 2 | # triggers on publication. 3 | # The expected workflow is to create a draft release and let the wheels 4 | # upload, and then hit 'publish', which uploads to PyPi. 5 | 6 | on: 7 | release: 8 | types: 9 | - published 10 | 11 | jobs: 12 | upload_pypi: 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: pypi 16 | url: https://pypi.org/p/cymem 17 | permissions: 18 | id-token: write 19 | contents: read 20 | if: github.event_name == 'release' && github.event.action == 'published' 21 | # or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this) 22 | # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 23 | steps: 24 | - uses: robinraju/release-downloader@v1 25 | with: 26 | tag: ${{ github.event.release.tag_name }} 27 | fileName: '*' 28 | out-file-path: 'dist' 29 | - uses: pypa/gh-action-pypi-publish@release/v1 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - '**' 7 | paths-ignore: 8 | - "*.md" 9 | - ".github/*" 10 | pull_request: 11 | types: [opened, synchronize, reopened, edited] 12 | paths-ignore: 13 | - "*.md" 14 | 15 | env: 16 | MODULE_NAME: 'cymem' 17 | RUN_MYPY: 'false' 18 | 19 | jobs: 20 | tests: 21 | name: Test 22 | if: github.repository_owner == 'explosion' 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 28 | exclude: 29 | # The 3.9/3.10 arm64 Python binaries that `setup-python` tries to use 30 | # don't work on macOS 15 anymore, so skip those. 31 | - os: macos-latest 32 | python_version: "3.9" 33 | - os: macos-latest 34 | python_version: "3.10" 35 | runs-on: ${{ matrix.os }} 36 | 37 | steps: 38 | - name: Check out repo 39 | uses: actions/checkout@v4 40 | 41 | - name: Configure Python version 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: ${{ matrix.python_version }} 45 | architecture: x64 46 | 47 | - name: Build sdist 48 | run: | 49 | python -m pip install -U build pip setuptools 50 | python -m pip install -U -r requirements.txt 51 | python -m build --sdist 52 | 53 | - name: Run mypy 54 | shell: bash 55 | if: ${{ env.RUN_MYPY == 'true' }} 56 | run: | 57 | python -m mypy $MODULE_NAME 58 | 59 | - name: Delete source directory 60 | shell: bash 61 | run: | 62 | rm -rf $MODULE_NAME 63 | 64 | - name: Uninstall all packages 65 | run: | 66 | python -m pip freeze > installed.txt 67 | python -m pip uninstall -y -r installed.txt 68 | 69 | - name: Install from sdist 70 | shell: bash 71 | run: | 72 | SDIST=$(python -c "import os;print(os.listdir('./dist')[-1])" 2>&1) 73 | pip install dist/$SDIST 74 | 75 | - name: Test import 76 | shell: bash 77 | run: | 78 | python -c "import $MODULE_NAME" -Werror 79 | 80 | - name: Install test requirements 81 | run: | 82 | python -m pip install -U -r requirements.txt 83 | 84 | - name: Run tests 85 | shell: bash 86 | run: | 87 | python -m pytest --pyargs $MODULE_NAME -Werror 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cython / C extensions 2 | cythonize.json 3 | spacy/*.html 4 | *.cpp 5 | *.so 6 | 7 | # Vim / VSCode / editors 8 | *.swp 9 | *.sw* 10 | Profile.prof 11 | .vscode 12 | .sass-cache 13 | 14 | # Python 15 | .Python 16 | .python-version 17 | __pycache__/ 18 | .pytest_cache 19 | *.py[cod] 20 | .env/ 21 | .env* 22 | .~env/ 23 | .venv 24 | venv/ 25 | .dev 26 | .denv 27 | .pypyenv 28 | .pytest_cache/ 29 | 30 | # Distribution / packaging 31 | env/ 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | *.egg-info/ 42 | pip-wheel-metadata/ 43 | Pipfile.lock 44 | .installed.cfg 45 | *.egg 46 | .eggs 47 | MANIFEST 48 | 49 | # Temporary files 50 | *.~* 51 | tmp/ 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .coverage 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | 65 | # Translations 66 | *.mo 67 | 68 | # Mr Developer 69 | .mr.developer.cfg 70 | .project 71 | .pydevproject 72 | 73 | # Rope 74 | .ropeproject 75 | 76 | # Django stuff: 77 | *.log 78 | *.pot 79 | 80 | # Windows 81 | *.bat 82 | Thumbs.db 83 | Desktop.ini 84 | 85 | # Mac OS X 86 | *.DS_Store 87 | 88 | # Komodo project files 89 | *.komodoproject 90 | 91 | # Other 92 | *.tgz 93 | 94 | # Pycharm project files 95 | *.idea 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2016-2022 ExplosionAI GmbH, 2014 Matthew Honnibal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-exclude cymem *.cpp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # cymem: A Cython Memory Helper 4 | 5 | cymem provides two small memory-management helpers for Cython. They make it easy 6 | to tie memory to a Python object's life-cycle, so that the memory is freed when 7 | the object is garbage collected. 8 | 9 | [![tests](https://github.com/explosion/cymem/actions/workflows/tests.yml/badge.svg)](https://github.com/explosion/cymem/actions/workflows/tests.yml) 10 | [![pypi Version](https://img.shields.io/pypi/v/cymem.svg?style=flat-square&logo=pypi&logoColor=white)](https://pypi.python.org/pypi/cymem) 11 | [![conda Version](https://img.shields.io/conda/vn/conda-forge/cymem.svg?style=flat-square&logo=conda-forge&logoColor=white)](https://anaconda.org/conda-forge/cymem) 12 | [![Python wheels](https://img.shields.io/badge/wheels-%E2%9C%93-4c1.svg?longCache=true&style=flat-square&logo=python&logoColor=white)](https://github.com/explosion/wheelwright/releases) 13 | 14 | ## Overview 15 | 16 | The most useful is `cymem.Pool`, which acts as a thin wrapper around the calloc 17 | function: 18 | 19 | ```python 20 | from cymem.cymem cimport Pool 21 | cdef Pool mem = Pool() 22 | data1 = mem.alloc(10, sizeof(int)) 23 | data2 = mem.alloc(12, sizeof(float)) 24 | ``` 25 | 26 | The `Pool` object saves the memory addresses internally, and frees them when the 27 | object is garbage collected. Typically you'll attach the `Pool` to some cdef'd 28 | class. This is particularly handy for deeply nested structs, which have 29 | complicated initialization functions. Just pass the `Pool` object into the 30 | initializer, and you don't have to worry about freeing your struct at all — all 31 | of the calls to `Pool.alloc` will be automatically freed when the `Pool` 32 | expires. 33 | 34 | ## Installation 35 | 36 | Installation is via [pip](https://pypi.python.org/pypi/pip), and requires 37 | [Cython](http://cython.org). Before installing, make sure that your `pip`, 38 | `setuptools` and `wheel` are up to date. 39 | 40 | ```bash 41 | pip install -U pip setuptools wheel 42 | pip install cymem 43 | ``` 44 | 45 | ## Example Use Case: An array of structs 46 | 47 | Let's say we want a sequence of sparse matrices. We need fast access, and a 48 | Python list isn't performing well enough. So, we want a C-array or C++ vector, 49 | which means we need the sparse matrix to be a C-level struct — it can't be a 50 | Python class. We can write this easily enough in Cython: 51 | 52 | ```python 53 | """Example without Cymem 54 | 55 | To use an array of structs, we must carefully walk the data structure when 56 | we deallocate it. 57 | """ 58 | 59 | from libc.stdlib cimport calloc, free 60 | 61 | cdef struct SparseRow: 62 | size_t length 63 | size_t* indices 64 | double* values 65 | 66 | cdef struct SparseMatrix: 67 | size_t length 68 | SparseRow* rows 69 | 70 | cdef class MatrixArray: 71 | cdef size_t length 72 | cdef SparseMatrix** matrices 73 | 74 | def __cinit__(self, list py_matrices): 75 | self.length = 0 76 | self.matrices = NULL 77 | 78 | def __init__(self, list py_matrices): 79 | self.length = len(py_matrices) 80 | self.matrices = calloc(len(py_matrices), sizeof(SparseMatrix*)) 81 | 82 | for i, py_matrix in enumerate(py_matrices): 83 | self.matrices[i] = sparse_matrix_init(py_matrix) 84 | 85 | def __dealloc__(self): 86 | for i in range(self.length): 87 | sparse_matrix_free(self.matrices[i]) 88 | free(self.matrices) 89 | 90 | 91 | cdef SparseMatrix* sparse_matrix_init(list py_matrix) except NULL: 92 | sm = calloc(1, sizeof(SparseMatrix)) 93 | sm.length = len(py_matrix) 94 | sm.rows = calloc(sm.length, sizeof(SparseRow)) 95 | cdef size_t i, j 96 | cdef dict py_row 97 | cdef size_t idx 98 | cdef double value 99 | for i, py_row in enumerate(py_matrix): 100 | sm.rows[i].length = len(py_row) 101 | sm.rows[i].indices = calloc(sm.rows[i].length, sizeof(size_t)) 102 | sm.rows[i].values = calloc(sm.rows[i].length, sizeof(double)) 103 | for j, (idx, value) in enumerate(py_row.items()): 104 | sm.rows[i].indices[j] = idx 105 | sm.rows[i].values[j] = value 106 | return sm 107 | 108 | 109 | cdef void* sparse_matrix_free(SparseMatrix* sm) except *: 110 | cdef size_t i 111 | for i in range(sm.length): 112 | free(sm.rows[i].indices) 113 | free(sm.rows[i].values) 114 | free(sm.rows) 115 | free(sm) 116 | ``` 117 | 118 | We wrap the data structure in a Python ref-counted class at as low a level as we 119 | can, given our performance constraints. This allows us to allocate and free the 120 | memory in the `__cinit__` and `__dealloc__` Cython special methods. 121 | 122 | However, it's very easy to make mistakes when writing the `__dealloc__` and 123 | `sparse_matrix_free` functions, leading to memory leaks. cymem prevents you from 124 | writing these deallocators at all. Instead, you write as follows: 125 | 126 | ```python 127 | """Example with Cymem. 128 | 129 | Memory allocation is hidden behind the Pool class, which remembers the 130 | addresses it gives out. When the Pool object is garbage collected, all of 131 | its addresses are freed. 132 | 133 | We don't need to write MatrixArray.__dealloc__ or sparse_matrix_free, 134 | eliminating a common class of bugs. 135 | """ 136 | from cymem.cymem cimport Pool 137 | 138 | cdef struct SparseRow: 139 | size_t length 140 | size_t* indices 141 | double* values 142 | 143 | cdef struct SparseMatrix: 144 | size_t length 145 | SparseRow* rows 146 | 147 | 148 | cdef class MatrixArray: 149 | cdef size_t length 150 | cdef SparseMatrix** matrices 151 | cdef Pool mem 152 | 153 | def __cinit__(self, list py_matrices): 154 | self.mem = None 155 | self.length = 0 156 | self.matrices = NULL 157 | 158 | def __init__(self, list py_matrices): 159 | self.mem = Pool() 160 | self.length = len(py_matrices) 161 | self.matrices = self.mem.alloc(self.length, sizeof(SparseMatrix*)) 162 | for i, py_matrix in enumerate(py_matrices): 163 | self.matrices[i] = sparse_matrix_init(self.mem, py_matrix) 164 | 165 | cdef SparseMatrix* sparse_matrix_init_cymem(Pool mem, list py_matrix) except NULL: 166 | sm = mem.alloc(1, sizeof(SparseMatrix)) 167 | sm.length = len(py_matrix) 168 | sm.rows = mem.alloc(sm.length, sizeof(SparseRow)) 169 | cdef size_t i, j 170 | cdef dict py_row 171 | cdef size_t idx 172 | cdef double value 173 | for i, py_row in enumerate(py_matrix): 174 | sm.rows[i].length = len(py_row) 175 | sm.rows[i].indices = mem.alloc(sm.rows[i].length, sizeof(size_t)) 176 | sm.rows[i].values = mem.alloc(sm.rows[i].length, sizeof(double)) 177 | for j, (idx, value) in enumerate(py_row.items()): 178 | sm.rows[i].indices[j] = idx 179 | sm.rows[i].values[j] = value 180 | return sm 181 | ``` 182 | 183 | All that the `Pool` class does is remember the addresses it gives out. When the 184 | `MatrixArray` object is garbage-collected, the `Pool` object will also be 185 | garbage collected, which triggers a call to `Pool.__dealloc__`. The `Pool` then 186 | frees all of its addresses. This saves you from walking back over your nested 187 | data structures to free them, eliminating a common class of errors. 188 | 189 | ## Custom Allocators 190 | 191 | Sometimes external C libraries use private functions to allocate and free 192 | objects, but we'd still like the laziness of the `Pool`. 193 | 194 | ```python 195 | from cymem.cymem cimport Pool, WrapMalloc, WrapFree 196 | cdef Pool mem = Pool(WrapMalloc(priv_malloc), WrapFree(priv_free)) 197 | ``` 198 | -------------------------------------------------------------------------------- /bin/push-tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Insist repository is clean 6 | git diff-index --quiet HEAD 7 | 8 | git checkout master 9 | git pull origin master 10 | git push origin master 11 | version=$(grep "__version__ = " cymem/about.py) 12 | version=${version/__version__ = } 13 | version=${version/\'/} 14 | version=${version/\'/} 15 | git tag "v$version" 16 | git push origin --tags 17 | -------------------------------------------------------------------------------- /cymem/__init__.pxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/explosion/cymem/8e2deff37552b0db1996539e4903799925ea503c/cymem/__init__.pxd -------------------------------------------------------------------------------- /cymem/__init__.py: -------------------------------------------------------------------------------- 1 | from .about import * 2 | -------------------------------------------------------------------------------- /cymem/about.py: -------------------------------------------------------------------------------- 1 | __title__ = "cymem" 2 | __version__ = "2.0.10" 3 | __summary__ = "Manage calls to calloc/free through Cython" 4 | __uri__ = "https://github.com/explosion/cymem" 5 | __author__ = "Matthew Honnibal" 6 | __email__ = "matt@explosion.ai" 7 | __license__ = "MIT" 8 | -------------------------------------------------------------------------------- /cymem/cymem.pxd: -------------------------------------------------------------------------------- 1 | ctypedef void* (*malloc_t)(size_t n) 2 | ctypedef void (*free_t)(void *p) 3 | 4 | cdef class PyMalloc: 5 | cdef malloc_t malloc 6 | cdef void _set(self, malloc_t malloc) 7 | 8 | cdef PyMalloc WrapMalloc(malloc_t malloc) 9 | 10 | cdef class PyFree: 11 | cdef free_t free 12 | cdef void _set(self, free_t free) 13 | 14 | cdef PyFree WrapFree(free_t free) 15 | 16 | cdef class Pool: 17 | cdef readonly size_t size 18 | cdef readonly dict addresses 19 | cdef readonly list refs 20 | cdef readonly PyMalloc pymalloc 21 | cdef readonly PyFree pyfree 22 | 23 | cdef void* alloc(self, size_t number, size_t size) except NULL 24 | cdef void free(self, void* addr) except * 25 | cdef void* realloc(self, void* addr, size_t n) except NULL 26 | 27 | 28 | cdef class Address: 29 | cdef void* ptr 30 | cdef readonly PyMalloc pymalloc 31 | cdef readonly PyFree pyfree 32 | -------------------------------------------------------------------------------- /cymem/cymem.pyx: -------------------------------------------------------------------------------- 1 | # cython: embedsignature=True 2 | 3 | from cpython.mem cimport PyMem_Malloc, PyMem_Free 4 | from cpython.ref cimport Py_INCREF, Py_DECREF 5 | from libc.string cimport memset 6 | from libc.string cimport memcpy 7 | import warnings 8 | 9 | WARN_ZERO_ALLOC = False 10 | 11 | cdef class PyMalloc: 12 | cdef void _set(self, malloc_t malloc): 13 | self.malloc = malloc 14 | 15 | cdef PyMalloc WrapMalloc(malloc_t malloc): 16 | cdef PyMalloc o = PyMalloc() 17 | o._set(malloc) 18 | return o 19 | 20 | cdef class PyFree: 21 | cdef void _set(self, free_t free): 22 | self.free = free 23 | 24 | cdef PyFree WrapFree(free_t free): 25 | cdef PyFree o = PyFree() 26 | o._set(free) 27 | return o 28 | 29 | Default_Malloc = WrapMalloc(PyMem_Malloc) 30 | Default_Free = WrapFree(PyMem_Free) 31 | 32 | cdef class Pool: 33 | """Track allocated memory addresses, and free them all when the Pool is 34 | garbage collected. This provides an easy way to avoid memory leaks, and 35 | removes the need for deallocation functions for complicated structs. 36 | 37 | >>> from cymem.cymem cimport Pool 38 | >>> cdef Pool mem = Pool() 39 | >>> data1 = mem.alloc(10, sizeof(int)) 40 | >>> data2 = mem.alloc(12, sizeof(float)) 41 | 42 | Attributes: 43 | size (size_t): The current size (in bytes) allocated by the pool. 44 | addresses (dict): The currently allocated addresses and their sizes. Read-only. 45 | pymalloc (PyMalloc): The allocator to use (default uses PyMem_Malloc). 46 | pyfree (PyFree): The free to use (default uses PyMem_Free). 47 | """ 48 | 49 | def __cinit__(self, PyMalloc pymalloc=Default_Malloc, 50 | PyFree pyfree=Default_Free): 51 | self.size = 0 52 | self.addresses = {} 53 | self.refs = [] 54 | self.pymalloc = pymalloc 55 | self.pyfree = pyfree 56 | 57 | def __dealloc__(self): 58 | cdef size_t addr 59 | if self.addresses is not None: 60 | for addr in self.addresses: 61 | if addr != 0: 62 | self.pyfree.free(addr) 63 | 64 | cdef void* alloc(self, size_t number, size_t elem_size) except NULL: 65 | """Allocate a 0-initialized number*elem_size-byte block of memory, and 66 | remember its address. The block will be freed when the Pool is garbage 67 | collected. Throw warning when allocating zero-length size and 68 | WARN_ZERO_ALLOC was set to True. 69 | """ 70 | if WARN_ZERO_ALLOC and (number == 0 or elem_size == 0): 71 | warnings.warn("Allocating zero bytes") 72 | cdef void* p = self.pymalloc.malloc(number * elem_size) 73 | if p == NULL: 74 | raise MemoryError("Error assigning %d bytes" % (number * elem_size)) 75 | memset(p, 0, number * elem_size) 76 | self.addresses[p] = number * elem_size 77 | self.size += number * elem_size 78 | return p 79 | 80 | cdef void* realloc(self, void* p, size_t new_size) except NULL: 81 | """Resizes the memory block pointed to by p to new_size bytes, returning 82 | a non-NULL pointer to the new block. new_size must be larger than the 83 | original. 84 | 85 | If p is not in the Pool or new_size is 0, a MemoryError is raised. 86 | """ 87 | if p not in self.addresses: 88 | raise ValueError("Pointer %d not found in Pool %s" % (p, self.addresses)) 89 | if new_size == 0: 90 | raise ValueError("Realloc requires new_size > 0") 91 | assert new_size > self.addresses[p] 92 | cdef void* new_ptr = self.alloc(1, new_size) 93 | if new_ptr == NULL: 94 | raise MemoryError("Error reallocating to %d bytes" % new_size) 95 | memcpy(new_ptr, p, self.addresses[p]) 96 | self.free(p) 97 | self.addresses[new_ptr] = new_size 98 | return new_ptr 99 | 100 | cdef void free(self, void* p) except *: 101 | """Frees the memory block pointed to by p, which must have been returned 102 | by a previous call to Pool.alloc. You don't necessarily need to free 103 | memory addresses manually --- you can instead let the Pool be garbage 104 | collected, at which point all the memory will be freed. 105 | 106 | If p is not in Pool.addresses, a KeyError is raised. 107 | """ 108 | self.size -= self.addresses.pop(p) 109 | self.pyfree.free(p) 110 | 111 | def own_pyref(self, object py_ref): 112 | self.refs.append(py_ref) 113 | 114 | 115 | cdef class Address: 116 | """A block of number * size-bytes of 0-initialized memory, tied to a Python 117 | ref-counted object. When the object is garbage collected, the memory is freed. 118 | 119 | >>> from cymem.cymem cimport Address 120 | >>> cdef Address address = Address(10, sizeof(double)) 121 | >>> d10 = address.ptr 122 | 123 | Args: 124 | number (size_t): The number of elements in the memory block. 125 | elem_size (size_t): The size of each element. 126 | 127 | Attributes: 128 | ptr (void*): Pointer to the memory block. 129 | addr (size_t): Read-only size_t cast of the pointer. 130 | pymalloc (PyMalloc): The allocator to use (default uses PyMem_Malloc). 131 | pyfree (PyFree): The free to use (default uses PyMem_Free). 132 | """ 133 | def __cinit__(self, size_t number, size_t elem_size, 134 | PyMalloc pymalloc=Default_Malloc, PyFree pyfree=Default_Free): 135 | self.ptr = NULL 136 | self.pymalloc = pymalloc 137 | self.pyfree = pyfree 138 | 139 | def __init__(self, size_t number, size_t elem_size): 140 | self.ptr = self.pymalloc.malloc(number * elem_size) 141 | if self.ptr == NULL: 142 | raise MemoryError("Error assigning %d bytes" % number * elem_size) 143 | memset(self.ptr, 0, number * elem_size) 144 | 145 | property addr: 146 | def __get__(self): 147 | return self.ptr 148 | 149 | def __dealloc__(self): 150 | if self.ptr != NULL: 151 | self.pyfree.free(self.ptr) 152 | -------------------------------------------------------------------------------- /cymem/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/explosion/cymem/8e2deff37552b0db1996539e4903799925ea503c/cymem/tests/__init__.py -------------------------------------------------------------------------------- /cymem/tests/test_import.py: -------------------------------------------------------------------------------- 1 | # This is a very weak test, but testing Cython code can be hard. So, at least check 2 | # we can create the object... 3 | 4 | from cymem.cymem import Pool, Address 5 | 6 | 7 | def test_pool(): 8 | mem = Pool() 9 | assert mem.size == 0 10 | 11 | def test_address(): 12 | address = Address(1, 2) 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | "cython>=0.25", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.cibuildwheel] 9 | build = "*" 10 | skip = "pp* cp36* cp37* cp38*" 11 | test-skip = "" 12 | free-threaded-support = false 13 | 14 | archs = ["native"] 15 | 16 | build-frontend = "default" 17 | config-settings = {} 18 | dependency-versions = "pinned" 19 | environment = {} 20 | environment-pass = [] 21 | build-verbosity = 0 22 | 23 | before-all = "" 24 | before-build = "" 25 | repair-wheel-command = "" 26 | 27 | test-command = "" 28 | before-test = "" 29 | test-requires = [] 30 | test-extras = [] 31 | 32 | container-engine = "docker" 33 | 34 | manylinux-x86_64-image = "manylinux2014" 35 | manylinux-i686-image = "manylinux2014" 36 | manylinux-aarch64-image = "manylinux2014" 37 | manylinux-ppc64le-image = "manylinux2014" 38 | manylinux-s390x-image = "manylinux2014" 39 | manylinux-pypy_x86_64-image = "manylinux2014" 40 | manylinux-pypy_i686-image = "manylinux2014" 41 | manylinux-pypy_aarch64-image = "manylinux2014" 42 | 43 | musllinux-x86_64-image = "musllinux_1_2" 44 | musllinux-i686-image = "musllinux_1_2" 45 | musllinux-aarch64-image = "musllinux_1_2" 46 | musllinux-ppc64le-image = "musllinux_1_2" 47 | musllinux-s390x-image = "musllinux_1_2" 48 | 49 | 50 | [tool.cibuildwheel.linux] 51 | repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" 52 | 53 | [tool.cibuildwheel.macos] 54 | repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}" 55 | 56 | [tool.cibuildwheel.windows] 57 | 58 | [tool.cibuildwheel.pyodide] 59 | 60 | 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cython>=0.25 2 | pytest 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import io 4 | import os 5 | import sys 6 | import contextlib 7 | from setuptools import Extension, setup, find_packages 8 | from setuptools.command.build_ext import build_ext 9 | from sysconfig import get_path 10 | from Cython.Build import cythonize 11 | 12 | 13 | PACKAGES = find_packages() 14 | MOD_NAMES = ["cymem.cymem"] 15 | 16 | 17 | # By subclassing build_extensions we have the actual compiler that will be used which is really known only after finalize_options 18 | # http://stackoverflow.com/questions/724664/python-distutils-how-to-get-a-compiler-that-is-going-to-be-used 19 | compile_options = { 20 | "msvc": ["/Ox", "/EHsc"], 21 | "other": ["-O3", "-Wno-strict-prototypes", "-Wno-unused-function"], 22 | } 23 | link_options = {"msvc": [], "other": []} 24 | 25 | 26 | class build_ext_options: 27 | def build_options(self): 28 | for e in self.extensions: 29 | e.extra_compile_args = compile_options.get( 30 | self.compiler.compiler_type, compile_options["other"] 31 | ) 32 | for e in self.extensions: 33 | e.extra_link_args = link_options.get( 34 | self.compiler.compiler_type, link_options["other"] 35 | ) 36 | 37 | 38 | class build_ext_subclass(build_ext, build_ext_options): 39 | def build_extensions(self): 40 | build_ext_options.build_options(self) 41 | build_ext.build_extensions(self) 42 | 43 | 44 | def clean(path): 45 | for name in MOD_NAMES: 46 | name = name.replace(".", "/") 47 | for ext in [".so", ".html", ".cpp", ".c"]: 48 | file_path = os.path.join(path, name + ext) 49 | if os.path.exists(file_path): 50 | os.unlink(file_path) 51 | 52 | 53 | @contextlib.contextmanager 54 | def chdir(new_dir): 55 | old_dir = os.getcwd() 56 | try: 57 | os.chdir(new_dir) 58 | sys.path.insert(0, new_dir) 59 | yield 60 | finally: 61 | del sys.path[0] 62 | os.chdir(old_dir) 63 | 64 | 65 | def setup_package(): 66 | root = os.path.abspath(os.path.dirname(__file__)) 67 | 68 | if len(sys.argv) > 1 and sys.argv[1] == "clean": 69 | return clean(root) 70 | 71 | with chdir(root): 72 | with io.open(os.path.join(root, "cymem", "about.py"), encoding="utf8") as f: 73 | about = {} 74 | exec(f.read(), about) 75 | 76 | with io.open(os.path.join(root, "README.md"), encoding="utf8") as f: 77 | readme = f.read() 78 | 79 | include_dirs = [get_path("include")] 80 | 81 | ext_modules = [] 82 | for mod_name in MOD_NAMES: 83 | mod_path = mod_name.replace(".", "/") + ".pyx" 84 | ext_modules.append( 85 | Extension( 86 | mod_name, [mod_path], language="c++", include_dirs=include_dirs 87 | ) 88 | ) 89 | 90 | setup( 91 | name="cymem", 92 | zip_safe=False, 93 | packages=PACKAGES, 94 | package_data={"": ["*.pyx", "*.pxd"]}, 95 | description=about["__summary__"], 96 | long_description=readme, 97 | long_description_content_type="text/markdown", 98 | author=about["__author__"], 99 | author_email=about["__email__"], 100 | version=about["__version__"], 101 | url=about["__uri__"], 102 | license=about["__license__"], 103 | ext_modules=cythonize(ext_modules, language_level=2), 104 | setup_requires=["cython>=0.25"], 105 | classifiers=[ 106 | "Environment :: Console", 107 | "Intended Audience :: Developers", 108 | "Intended Audience :: Science/Research", 109 | "License :: OSI Approved :: MIT License", 110 | "Operating System :: POSIX :: Linux", 111 | "Operating System :: MacOS :: MacOS X", 112 | "Operating System :: Microsoft :: Windows", 113 | "Programming Language :: Cython", 114 | "Programming Language :: Python :: 3.9", 115 | "Programming Language :: Python :: 3.10", 116 | "Programming Language :: Python :: 3.11", 117 | "Programming Language :: Python :: 3.12", 118 | "Programming Language :: Python :: 3.13", 119 | "Topic :: Scientific/Engineering", 120 | ], 121 | cmdclass={"build_ext": build_ext_subclass}, 122 | ) 123 | 124 | 125 | if __name__ == "__main__": 126 | setup_package() 127 | --------------------------------------------------------------------------------