├── .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 | [](https://github.com/explosion/cymem/actions/workflows/tests.yml)
10 | [](https://pypi.python.org/pypi/cymem)
11 | [](https://anaconda.org/conda-forge/cymem)
12 | [](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 |
--------------------------------------------------------------------------------