vcraft 1.4.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. vcraft-1.4.0.dist-info/METADATA +1753 -0
  2. vcraft-1.4.0.dist-info/RECORD +62 -0
  3. vcraft-1.4.0.dist-info/WHEEL +4 -0
  4. vcraft-1.4.0.dist-info/entry_points.txt +2 -0
  5. vcraft-1.4.0.dist-info/licenses/LICENSE +21 -0
  6. vcraft_tool/__init__.py +2 -0
  7. vcraft_tool/__main__.py +4 -0
  8. vcraft_tool/_launch.py +34 -0
  9. vcraft_tool/bin/vcraft.exe +0 -0
  10. vcraft_tool/vlib/vcraft/README.md +544 -0
  11. vcraft_tool/vlib/vcraft/args.v +572 -0
  12. vcraft_tool/vlib/vcraft/buffer.v +905 -0
  13. vcraft_tool/vlib/vcraft/c/gc_preinit.h +257 -0
  14. vcraft_tool/vlib/vcraft/c/shim.c +1237 -0
  15. vcraft_tool/vlib/vcraft/c/shim.h +208 -0
  16. vcraft_tool/vlib/vcraft/callback.v +62 -0
  17. vcraft_tool/vlib/vcraft/class.v +917 -0
  18. vcraft_tool/vlib/vcraft/convert.v +557 -0
  19. vcraft_tool/vlib/vcraft/cpython.c.v +621 -0
  20. vcraft_tool/vlib/vcraft/cstring.v +84 -0
  21. vcraft_tool/vlib/vcraft/enum.v +68 -0
  22. vcraft_tool/vlib/vcraft/errors.v +334 -0
  23. vcraft_tool/vlib/vcraft/gil.v +68 -0
  24. vcraft_tool/vlib/vcraft/module.v +300 -0
  25. vcraft_tool/vlib/vcraft/object.v +282 -0
  26. vcraft_tool/vlib/vcraft/v.mod +7 -0
  27. vcraft_tool/vlib/vcraft_ci/v.mod +7 -0
  28. vcraft_tool/vlib/vcraft_ci/workflow.v +384 -0
  29. vcraft_tool/vlib/vcraft_codegen/README.md +299 -0
  30. vcraft_tool/vlib/vcraft_codegen/collect.v +2015 -0
  31. vcraft_tool/vlib/vcraft_codegen/emit.v +1730 -0
  32. vcraft_tool/vlib/vcraft_codegen/emit_class.v +1239 -0
  33. vcraft_tool/vlib/vcraft_codegen/generate.v +111 -0
  34. vcraft_tool/vlib/vcraft_codegen/model.v +815 -0
  35. vcraft_tool/vlib/vcraft_codegen/scan.v +332 -0
  36. vcraft_tool/vlib/vcraft_codegen/types.v +274 -0
  37. vcraft_tool/vlib/vcraft_codegen/v.mod +7 -0
  38. vcraft_tool/vlib/vcraft_project/README.md +304 -0
  39. vcraft_tool/vlib/vcraft_project/backend.v +330 -0
  40. vcraft_tool/vlib/vcraft_project/build.v +918 -0
  41. vcraft_tool/vlib/vcraft_project/env.v +324 -0
  42. vcraft_tool/vlib/vcraft_project/project.v +295 -0
  43. vcraft_tool/vlib/vcraft_project/scaffold.v +306 -0
  44. vcraft_tool/vlib/vcraft_project/sdist.v +112 -0
  45. vcraft_tool/vlib/vcraft_project/target.v +330 -0
  46. vcraft_tool/vlib/vcraft_project/toml.v +566 -0
  47. vcraft_tool/vlib/vcraft_project/toolchain.v +188 -0
  48. vcraft_tool/vlib/vcraft_project/v.mod +7 -0
  49. vcraft_tool/vlib/vcraft_wheel/README.md +121 -0
  50. vcraft_tool/vlib/vcraft_wheel/buffer.v +57 -0
  51. vcraft_tool/vlib/vcraft_wheel/build.v +167 -0
  52. vcraft_tool/vlib/vcraft_wheel/crc32.v +49 -0
  53. vcraft_tool/vlib/vcraft_wheel/deflate.v +86 -0
  54. vcraft_tool/vlib/vcraft_wheel/fixed.v +284 -0
  55. vcraft_tool/vlib/vcraft_wheel/inflate.v +250 -0
  56. vcraft_tool/vlib/vcraft_wheel/metadata.v +107 -0
  57. vcraft_tool/vlib/vcraft_wheel/sha256.v +171 -0
  58. vcraft_tool/vlib/vcraft_wheel/tables.v +123 -0
  59. vcraft_tool/vlib/vcraft_wheel/tags.v +111 -0
  60. vcraft_tool/vlib/vcraft_wheel/tar.v +264 -0
  61. vcraft_tool/vlib/vcraft_wheel/v.mod +7 -0
  62. vcraft_tool/vlib/vcraft_wheel/zip.v +153 -0
@@ -0,0 +1,1753 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcraft
3
+ Version: 1.4.0
4
+ Summary: Native Python extensions written in V: CPython bindings, a code generator and a wheel builder in one tool
5
+ Keywords: v,vlang,python-extension,cpython,bindings,ffi,wheel,build-backend,pyo3,maturin
6
+ Author: Ricardo Robles
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Project-URL: Homepage, https://github.com/rroblf01/vcraft
10
+ Project-URL: Source, https://github.com/rroblf01/vcraft
11
+ Project-URL: Documentation, https://github.com/rroblf01/vcraft#readme
12
+ Project-URL: Changelog, https://github.com/rroblf01/vcraft/blob/main/CHANGELOG.md
13
+ Project-URL: Issues, https://github.com/rroblf01/vcraft/issues
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Other
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Programming Language :: Python :: 3.15
27
+ Classifier: Programming Language :: Python :: Implementation :: CPython
28
+ Classifier: Topic :: Software Development :: Build Tools
29
+ Classifier: Topic :: Software Development :: Code Generators
30
+ Classifier: Topic :: Software Development :: Compilers
31
+ Classifier: Topic :: System :: Software Distribution
32
+ Requires-Python: >=3.11
33
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
34
+
35
+ # vcraft
36
+
37
+ **Native Python extensions written in V.**
38
+
39
+ > **Documentation:** a guide and a reference, in English and Spanish, are at
40
+ > <https://rroblf01.github.io/vcraft/> (<https://rroblf01.github.io/vcraft/es/>). This
41
+ > README stays the source of truth and every V example in it is built by the test suite.
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/vcraft.svg)](https://pypi.org/project/vcraft/)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/vcraft.svg)](https://pypi.org/project/vcraft/)
45
+ [![CI](https://github.com/rroblf01/vcraft/actions/workflows/ci.yml/badge.svg)](https://github.com/rroblf01/vcraft/actions/workflows/ci.yml)
46
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/rroblf01/vcraft/blob/main/LICENSE)
47
+
48
+ `vcraft` is to the [V language](https://vlang.io) what **PyO3 + maturin** is to Rust:
49
+ CPython bindings written in V, a code generator that turns annotated V declarations
50
+ into a Python API, and a build tool that compiles the result and writes a wheel you
51
+ can upload to PyPI. There is no Rust, no C++ and no zlib anywhere in the pipeline.
52
+
53
+ ```v
54
+ // src/my_extension.v
55
+ module my_extension
56
+
57
+ // Adds two integers and returns the result.
58
+ @[vc_fn]
59
+ pub fn add(a int, b int) int {
60
+ return a + b
61
+ }
62
+ ```
63
+
64
+ ```python
65
+ >>> import my_extension as m
66
+ >>> m.add(2, 3)
67
+ 5
68
+ >>> help(m.add)
69
+ add(a: int, b: int) -> int
70
+ Adds two integers and returns the result.
71
+ ```
72
+
73
+ ## Highlights
74
+
75
+ - **One annotation per declaration.** `@[vc_fn]`, `@[vc_class]`, `@[vc_method]`,
76
+ `@[vc_field]` and friends become functions, classes, methods and properties, with
77
+ docstrings and a generated `.pyi` stub for type checkers.
78
+ - **Fast calls.** The generated glue calls your V functions with their real types
79
+ through `METH_FASTCALL`: no boxing layer, no reflection. Call overhead is on par
80
+ with or below PyO3 in the [benchmarks](#performance).
81
+ - **Real Python semantics.** V errors become exceptions (custom exception classes
82
+ included), panics are caught instead of killing the interpreter, classes support
83
+ inheritance, equality, hashing, iteration and garbage-collected reference cycles.
84
+ - **Zero-copy and GIL-free.** `[]u8` and `[]string` parameters alias Python's buffers,
85
+ `@[vc_nogil]` releases the GIL around a call, and free-threaded CPython (3.13t,
86
+ 3.14t, 3.15t) is supported.
87
+ - **Wheels without the toolchain zoo.** `vcraft build` writes the wheel itself
88
+ (DEFLATE, ZIP, RECORD, tags) for regular, abi3 and free-threaded builds,
89
+ manylinux and musllinux, and refuses version combinations that would only fail
90
+ after install.
91
+ - **CI included.** `vcraft generate-ci` writes a GitHub Actions matrix that builds,
92
+ installs and imports every wheel, using published manylinux/musllinux images and a
93
+ reusable action.
94
+
95
+ ## Quick start
96
+
97
+ **1. Install vcraft** (Linux x86_64 or macOS arm64, Python 3.11+):
98
+
99
+ ```console
100
+ $ pip install vcraft
101
+ $ vcraft --version
102
+ ```
103
+
104
+ **2. Install the V compiler.** `pip` ships the tool, not the toolchain, the same way
105
+ maturin needs Rust. vcraft needs a V newer than the 0.5.2 release, so it builds the
106
+ commit it is tested with into its own cache (about five minutes, once; needs git,
107
+ make and a C compiler):
108
+
109
+ ```console
110
+ $ vcraft toolchain install
111
+ $ vcraft toolchain # which compiler vcraft uses, and whether it is the pinned one
112
+ ```
113
+
114
+ vcraft finds it there by itself. `VCRAFT_V=/path/to/v` uses another V instead.
115
+
116
+ You also need a C compiler (gcc or clang) and the CPython headers, which most Python
117
+ installs already include.
118
+
119
+ **3. Create, develop and build a project:**
120
+
121
+ ```console
122
+ $ vcraft new my_extension
123
+ $ cd my_extension
124
+ $ python -m venv .venv && source .venv/bin/activate
125
+ $ vcraft develop # build and install into the active venv
126
+ $ python main.py # the example `vcraft new` wrote
127
+ Hello, world!
128
+ 5
129
+ 1
130
+ parse_int: invalid literal for int()
131
+ $ python -m unittest # the test under tests/
132
+ $ vcraft build --release # dist/my_extension-0.1.0-cp314-cp314-<platform>.whl
133
+ ```
134
+
135
+ `vcraft new` writes a Python package `my_extension` that re-exports the native module
136
+ `my_extension_native`, so `import my_extension` is the public name. Pass
137
+ `--module my_extension` if you would rather have no separate native module at all.
138
+
139
+ **4. Ship it.** `vcraft generate-ci` writes a GitHub Actions workflow that builds a
140
+ wheel per supported Python and platform, installs and imports each one, and is ready
141
+ to publish to PyPI. See [Continuous integration](#continuous-integration).
142
+
143
+ ## Table of contents
144
+
145
+ - [Why](#why)
146
+ - [How it works](#how-it-works)
147
+ - [The annotation vocabulary](#the-annotation-vocabulary)
148
+ - [Type marshalling](#type-marshalling)
149
+ - [Keyword arguments and defaults](#keyword-arguments-and-defaults)
150
+ - [Classes and properties](#classes-and-properties)
151
+ - [Errors and panics](#errors-and-panics)
152
+ - [The command line](#the-command-line)
153
+ - [Wheels](#wheels)
154
+ - [Generated project layout](#generated-project-layout)
155
+ - [Continuous integration](#continuous-integration)
156
+ - [Configuration](#configuration)
157
+ - [Performance](#performance)
158
+ - [Project status](#project-status)
159
+ - [Built with vcraft](#built-with-vcraft)
160
+ - [Stability](#stability)
161
+ - [Development](#development)
162
+ - [Design notes](#design-notes)
163
+ - [License](#license)
164
+
165
+ ---
166
+
167
+ ## Why
168
+
169
+ V compiles to fast native code through a tiny C backend and has no runtime dependencies of its own. That makes it a good fit for native Python extensions, but there has been no equivalent of PyO3: writing to the CPython C API from V means hand-declaring every foreign function, hand-writing the module initialiser, and hand-rolling the wheel.
170
+
171
+ `vcraft` removes all three chores.
172
+
173
+ | | Rust ecosystem | vcraft |
174
+ | ------------------ | ------------------ | ------------------------------- |
175
+ | Native bindings | PyO3 | `vlib/vcraft` (written in V) |
176
+ | Binding generation | `#[pyfunction]` | `@[vc_fn]` |
177
+ | Class bindings | `#[pyclass]` | `@[vc_class]` |
178
+ | Error translation | `Result<T, E>` | `!T` / `error` / `recover()` |
179
+ | GIL handling | `Python::detach` | `@[vc_nogil]` |
180
+ | Build tool | maturin | `vcraft build` |
181
+ | Local install | `maturin develop` | `vcraft develop` |
182
+ | CI | `maturin-action` | `vcraft-action@v1` |
183
+ | Dependency install | `cargo add` | `v import` |
184
+
185
+ ---
186
+
187
+ ## How it works
188
+
189
+ A CPython extension module is nothing more than a shared library that exports a
190
+ single symbol, `PyInit_<name>`. V can already produce exactly that:
191
+
192
+ ```
193
+ v -shared -o my_extension/_core.cpython-314-x86_64-linux-gnu.so src/
194
+ ```
195
+
196
+ Three properties of V's `-shared` mode make this work cleanly:
197
+
198
+ 1. **Symbol control.** `-shared` compiles with `-fvisibility=hidden` and
199
+ `-Wl,--exclude-libs,ALL`, so only declarations carrying an `@[export: '...']`
200
+ attribute appear in the dynamic symbol table. `PyInit_my_extension` is
201
+ exported; nothing else is.
202
+ 2. **Automatic lifecycle.** V emits `_vinit_caller` and `_vcleanup_caller` as ELF
203
+ constructors and destructors, so the V runtime and its garbage collector are
204
+ initialised when CPython `dlopen`s the module and torn down when it is unloaded.
205
+ No manual init hook is needed.
206
+ 3. **No link against `libpython`.** Extension modules leave the `Py*` symbols
207
+ undefined; the dynamic linker resolves them against the already-loaded
208
+ `libpython` in the global scope. This is how every CPython extension works, and
209
+ it is why a wheel built for one CPython minor version stays tied to it.
210
+
211
+ On top of that, `vcraft` adds a code generator. It parses your V sources,
212
+ discovers the annotated declarations, and emits the glue that CPython needs:
213
+
214
+ ```
215
+ src/my_extension.v ──vcraft codegen──▶ src/_vcraft_generated.v
216
+ (and python/my_extension.pyi)
217
+ │
218
+ ▼
219
+ v -shared ──▶ my_extension.cpython-314-x86_64-linux-gnu.so
220
+ │
221
+ ▼
222
+ dist/my_extension-0.1.0-cp314-cp314-linux_x86_64.whl
223
+ ├── my_extension.cpython-314-x86_64-linux-gnu.so
224
+ └── my_extension.pyi
225
+ ```
226
+
227
+ The extension goes at the root of the wheel, not inside a directory named after it:
228
+ a directory with no `__init__.py` is a namespace package, and Python resolves one
229
+ without opening the files inside it, so the module would import empty.
230
+
231
+ The generated glue is a normal V file. It calls your functions with their real V
232
+ types, so a call from Python becomes a direct C call with no intermediate
233
+ marshalling layer and no boxing.
234
+
235
+ ---
236
+
237
+ ## The annotation vocabulary
238
+
239
+ Annotations are read from your source text. V keeps declaration attributes in the
240
+ type checker rather than in the parse tree, so `vcraft` reads the `@[...]` block
241
+ that immediately precedes each declaration. Any name works; these are the ones
242
+ `vcraft` recognises.
243
+
244
+ | Annotation | Applies to | Effect |
245
+ | --------------- | --------------- | ---------------------------------------------------------- |
246
+ | `@[vc_fn]` | `pub fn` | Exports the function as a module-level Python callable |
247
+ | `@[vc_class]` | `pub struct` | Creates a Python type backed by the V struct |
248
+ | `@[vc_method]` | methods | Adds the method to the class of its receiver |
249
+ | `@[vc_field]` | struct fields | Exposes the field as an attribute of the instance |
250
+ | `@[vc_buffer]` | struct fields | A persistent native `[]T` of scalars, freed at dealloc |
251
+ | `@[vc_buffer_typed]` | struct fields | With `@[vc_buffer]`: expose it as a typed `memoryview` |
252
+ | `@[vc_property]`| methods | Registers the method as a Python `property` |
253
+ | `@[vc_base]` | `pub struct` | Makes the class inherit the named one |
254
+ | `@[vc_ref]` | struct fields | Exposes the field as a strong reference to another instance |
255
+ | `@[vc_error]` | `pub struct` | Makes the struct usable as the error of a `!T` function |
256
+ | `@[vc_static]` | methods | Registers the method as a `staticmethod` |
257
+ | `@[vc_classmethod]` | methods | Registers the method as a `classmethod` |
258
+ | `@[vc_len]` | methods | The method is `__len__` (`len(instance)`, truthiness) |
259
+ | `@[vc_getitem]` | methods | The method is `__getitem__` (`instance[i]`) |
260
+ | `@[vc_contains]`| methods | The method is `__contains__` (`x in instance`) |
261
+ | `@[vc_eq]` | `pub fn` | The function is `__eq__` (`==`, `!=`) |
262
+ | `@[vc_lt]`, `@[vc_le]`, `@[vc_gt]`, `@[vc_ge]` | `pub fn` | The function is the matching ordering operator |
263
+ | `@[vc_add]`, `@[vc_sub]`, `@[vc_mul]`, `@[vc_truediv]`, `@[vc_floordiv]`, `@[vc_mod]`, `@[vc_pow]` | `pub fn` | The function is the matching arithmetic operator |
264
+ | `@[vc_lshift]`, `@[vc_rshift]`, `@[vc_and]`, `@[vc_or]`, `@[vc_xor]` | `pub fn` | The function is the matching bitwise operator |
265
+ | `@[vc_radd]`, `@[vc_rsub]`, … | `pub fn` | The reflected form, reached when the instance is the right operand |
266
+ | `@[vc_neg]`, `@[vc_pos]`, `@[vc_abs]`, `@[vc_invert]` | `pub fn` | The function is the unary operator |
267
+ | `@[vc_hash]` | `pub fn` | The function is `__hash__` |
268
+ | `@[vc_const]` | `pub const` | Exposes the constant as a module attribute |
269
+ | `@[vc_defaults]`| `pub fn`, methods | Default values: `@[vc_defaults: 'step=1, name="x"']` |
270
+ | `@[vc_raw]` | `pub fn` | Skips marshalling; you receive and return `voidptr` yourself |
271
+ | `@[vc_nogil]` | `pub fn` | Runs the call with the GIL released |
272
+ | `@[vc_memoryview]`| `pub fn`, methods | Returns a native slice as a writable `memoryview` (no copy) |
273
+ | `@[vc_iter]` | methods | Makes the instance its own iterator (`__iter__`) |
274
+ | `@[vc_next]` | methods | Produces one item per call (`__next__`) |
275
+
276
+ Names are taken from the V declaration, verbatim. Doc comments become `__doc__`, the
277
+ marshalled signature becomes `__text_signature__`, and the same source also produces
278
+ a `.pyi` stub, so `help()` and a type checker see the same thing.
279
+
280
+ The prefix is part of the name rather than a namespace because V rejects two
281
+ annotations sharing one: `@[vc.fn]` and `@[vc.raw]` on the same declaration is
282
+ `duplicate attribute 'vc'`.
283
+
284
+ ---
285
+
286
+ ## Type marshalling
287
+
288
+ Values are converted with direct CPython calls whenever a fast path exists — for
289
+ example a V `int` becomes `PyLong_AsLongLong`, not a round trip through a
290
+ generic value tree.
291
+
292
+ | V | Python | Notes |
293
+ | ------------------------------------- | ---------------------------- | -------------------------------------------------- |
294
+ | `bool` | `bool` | Any object in, by its truth value |
295
+ | `i8`, `i16`, `i32`, `int`, `i64`, `isize` | `int` | Range-checked: out of range raises OverflowError |
296
+ | `u8`, `u16`, `u32`, `u64`, `usize` | `int` | Negative or out of range raises OverflowError |
297
+ | `f32`, `f64` | `float` | An `int` is accepted too |
298
+ | `rune` | `int` | The code point |
299
+ | `string` | `str` | UTF-8 |
300
+ | `[]u8` | `bytes` | Any bytes-like object in, without copying it |
301
+ | `[]T` of the types above, or `string` | `list` | Any sequence in (a list, a tuple); a list out |
302
+ | `[N]T` of the types above | `list` | Any sequence of exactly N items in; a list out |
303
+ | `?T` of the types above | `T` or `None` | `none` is `None`, both ways |
304
+ | `map[K]T`, `K` a scalar or `string` | `dict` | Scalar or `str` keys; a new dict out |
305
+ | `(A, B, ...)`, as a result | `tuple` | A multi-value return |
306
+ | `voidptr` | any object | Borrowed; you own the reference |
307
+ | `vcraft.PyObj` (result) | any object | Owned; the reference goes to the caller |
308
+ | `&T`, where `T` is a `@[vc_class]` | `T` | An instance reference; accepts a subclass |
309
+ | an `enum` the project declares | that `enum.IntEnum` | Members keep their names and values |
310
+ | a plain `struct` the project declares | `dict` | Field names as keys |
311
+ | `void`, `!void` | `None` | |
312
+ | `!T` | `T`, or raises | See [Errors and panics](#errors-and-panics) |
313
+
314
+ Fields of a `@[vc_class]` struct and method parameters follow the same table.
315
+ Composites nest, so `?[]int`, `[][]int` and `map[string][]int` cross both ways as
316
+ nested `list`/`dict`/`None`. An `enum` the project declares becomes an
317
+ `enum.IntEnum` of the same name, so a value reads back as the member, and a
318
+ plain `struct` becomes a `dict` of its fields. V function types are not
319
+ supported yet; they are on the
320
+ [roadmap](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/ROADMAP.md). An instance reference (`&T`) is supported; see
321
+ [Instance references](#instance-references). A `@[vc_class]` is constructed from
322
+ Python with its class, through `new_<class>` when one is declared.
323
+
324
+ Anything not in this table is a compile-time diagnostic pointing at the exact
325
+ file, line and column, not a runtime surprise.
326
+
327
+ ---
328
+
329
+ ## Keyword arguments and defaults
330
+
331
+ Every parameter can be passed by position or by name, as in a Python function. A `?T`
332
+ parameter may be left out and arrives as `none`; any other parameter gets a default
333
+ from `@[vc_defaults]`, since V has no default arguments of its own:
334
+
335
+ ```v
336
+ // Formats a count, with an optional step and label.
337
+ @[vc_fn]
338
+ @[vc_defaults: 'step=1, label="item"']
339
+ pub fn describe(count int, step int, label string, unit ?string) string {
340
+ suffix := unit or { '' }
341
+ return '${label}: ${count * step}${suffix}'
342
+ }
343
+ ```
344
+
345
+ ```python
346
+ >>> m.describe(3)
347
+ 'item: 3'
348
+ >>> m.describe(3, label="box", unit="kg")
349
+ 'box: 3kg'
350
+ >>> m.describe()
351
+ TypeError: describe() missing required argument: 'count'
352
+ ```
353
+
354
+ Defaults are literals of the parameter's type (a number, `true`/`false`, or a quoted
355
+ string), for bool, integer, float and string parameters; anything else is a
356
+ diagnostic. A call that passes every parameter by position costs what it did before
357
+ keywords existed: one comparison.
358
+
359
+ ---
360
+
361
+ ## Classes and properties
362
+
363
+ ```v
364
+ // A counter with state.
365
+ //
366
+ // Fields must be scalars. A V string inside a Python object would be a pointer
367
+ // that V's collector cannot see, because it does not scan memory CPython
368
+ // allocated, so the string would be reclaimed while Python still held it. Reach a
369
+ // string through a method, which marshals it properly.
370
+ @[vc_class]
371
+ pub struct Counter {
372
+ mut:
373
+ // value is the running total.
374
+ @[vc_field] value int
375
+ @[vc_field] step int
376
+ }
377
+
378
+ // new_counter builds a Counter. It takes no arguments; the convention is that
379
+ // `new_<Class>` is the constructor, and an instance is made by calling the type.
380
+ pub fn new_counter() &Counter {
381
+ return &Counter{ step: 1 }
382
+ }
383
+
384
+ // increment adds step to value and returns the new total.
385
+ @[vc_method]
386
+ pub fn (mut c Counter) increment() int {
387
+ c.value += c.step
388
+ return c.value
389
+ }
390
+
391
+ // set_step changes how much each increment adds.
392
+ @[vc_method]
393
+ pub fn (mut c Counter) set_step(step int) {
394
+ c.step = step
395
+ }
396
+
397
+ // is_zero reports whether the value is still zero.
398
+ @[vc_method]
399
+ @[vc_property]
400
+ pub fn (c &Counter) is_zero() bool {
401
+ return c.value == 0
402
+ }
403
+ ```
404
+
405
+ ```python
406
+ >>> c = m.Counter()
407
+ >>> repr(c)
408
+ 'Counter(value: 0, step: 1)'
409
+ >>> c.increment(), c.increment()
410
+ (1, 2)
411
+ >>> c.step = 5
412
+ >>> c.increment()
413
+ 7
414
+ >>> c.is_zero
415
+ False
416
+ >>> c.value = 100
417
+ >>> c
418
+ Counter(value: 100, step: 5)
419
+ ```
420
+
421
+ A class becomes a CPython heap type created with `PyType_FromSpec`. The state lives
422
+ in a block CPython allocates, methods and accessors work on that block in place, and
423
+ `tp_dealloc` frees the block.
424
+
425
+ An `@[vc_eq]` and an `@[vc_hash]` function fill the type's comparison and hash slots.
426
+ They are free functions rather than methods, because V allows exactly one receiver per
427
+ method and a comparison needs both operands:
428
+
429
+ <!-- readme-test: continue -->
430
+ ```v
431
+ @[vc_eq]
432
+ pub fn counter_eq(a voidptr, b voidptr) bool {
433
+ mut x := Counter{}
434
+ mut y := Counter{}
435
+ vcraft.load_state(a, voidptr(&x), sizeof(Counter))
436
+ vcraft.load_state(b, voidptr(&y), sizeof(Counter))
437
+ return x.value == y.value && x.step == y.step
438
+ }
439
+
440
+ @[vc_hash]
441
+ pub fn counter_hash(self voidptr) int {
442
+ mut c := Counter{}
443
+ vcraft.load_state(self, voidptr(&c), sizeof(Counter))
444
+ return c.value * 31 + c.step
445
+ }
446
+ ```
447
+
448
+ ```python
449
+ >>> a, b = m.Counter(), m.Counter()
450
+ >>> a == b, {a, b}
451
+ (True, {Counter(value: 0, step: 1)})
452
+ ```
453
+
454
+ The two go together. Python's dicts assume that two objects which compare equal hash the
455
+ same, so a value comparison with an identity hash misses every lookup in a set or a dict
456
+ key without reporting anything. Only `==` and `!=` reach the function; the ordering
457
+ operators return `NotImplemented`, which is what lets Python try the other operand's
458
+ reflected method before failing with an error that names the type. Comparing against
459
+ another type is `False` rather than a `TypeError`, which is what `NotImplemented` buys.
460
+
461
+ An `@[vc_field]` becomes a read/write attribute. Assigning the wrong type raises
462
+ `TypeError`, and deleting one raises `AttributeError`: both come for free from
463
+ registering a setter, rather than from a hand-written check per field. An
464
+ `@[vc_property]` method becomes a read-only property, and a plain `@[vc_method]`
465
+ method takes arguments like any other exposed function.
466
+
467
+ An annotation goes on the field's own line, `@[vc_field] value int`. V also accepts an
468
+ attribute on the line above, but vcraft reads the field's line and nothing else, so an
469
+ `@[vc_field]` written there leaves the field unexposed. Rather than let it vanish, the
470
+ generator warns and names the field, and still builds.
471
+
472
+ The constructor runs in `tp_new`, and it may take arguments: `new_vector(4)` is called by
473
+ `Vector(4)`. They are read from the tuple and dict Python passes `tp_new` with the same
474
+ marshalling rules a method's parameters use, so keyword arguments and `@[vc_defaults]`
475
+ work, and the signature reaches the `.pyi` stub as `__init__`. An argument-free
476
+ constructor is still a `TypeError` when called with arguments, as before.
477
+
478
+ A subclass constructor carries its base's parameters as well, in front of its own:
479
+ `newstate` builds the base's half of the state first, so `tp_new` binds the base's
480
+ parameters too and forwards them on. With `new_root(n int)` and `new_mid(m int)`,
481
+ `Mid(10, 3)` builds `Root` with `10` and `Mid` with `3`, and a subclass with no
482
+ constructor of its own still accepts its base's parameters (so `Mid`'s own `new_mid`
483
+ takes only `m`). Because both sets travel in the one Python signature the subclass binds,
484
+ they need distinct names; a shared name is reported.
485
+
486
+ Docstrings reach `__doc__` on the type, its methods, its properties and its fields.
487
+
488
+ ### Inheritance
489
+
490
+ `@[vc_base(Name)]` makes a class inherit another. Declaration order does not
491
+ matter: a subclass may be written before its base, or in a file that sorts
492
+ earlier, and the generator orders the classes itself.
493
+
494
+ <!-- readme-test: continue -->
495
+ ```v
496
+ @[vc_class]
497
+ @[vc_base(Counter)]
498
+ pub struct BoundedCounter {
499
+ mut:
500
+ @[vc_field] limit int
501
+ }
502
+
503
+ // bump adds `by`, refusing to pass the limit.
504
+ //
505
+ // The receiver is `BoundedCounter`, which names only `limit`: V has no struct
506
+ // inheritance, so `c.value` does not compile here. `vcraft.state_at(1)` is the
507
+ // base's own struct inside the live state of the instance the method is running
508
+ // on.
509
+ @[vc_method]
510
+ pub fn (mut c BoundedCounter) bump(by int) !int {
511
+ mut base := unsafe { &Counter(vcraft.state_at(1)) }
512
+ if base.value + by > c.limit {
513
+ return vcraft.raise_domain(.value_error, 'the counter would pass its limit')
514
+ }
515
+ base.value += by
516
+ return base.value
517
+ }
518
+ ```
519
+
520
+ What Python sees is ordinary single inheritance: one base, the base's fields,
521
+ methods and properties on the subclass, the subclass's own alongside them,
522
+ `isinstance` in both directions behaving as it should, and a Python subclass on
523
+ top of it working too.
524
+
525
+ Two details are worth knowing.
526
+
527
+ `state_at` takes a level, not a name. Level 1 is the immediate base, level 2 the
528
+ one above it, and so on, so a chain three deep can reach its root from the
529
+ grandchild. It is a level rather than a base because the offsets are not uniform:
530
+ each generation's state is its own base followed by its own struct, so where a
531
+ generation's struct sits depends on how many generations are above it. The
532
+ generator computes the address and publishes it; a method asks for the level it
533
+ wants. Eight levels is the cap, and a deeper chain is reported as an error rather
534
+ than silently truncated.
535
+
536
+ The pointer is only valid while the method runs. It points into the instance's
537
+ inline state, which the method works on in place.
538
+ There is nowhere to put it on the receiver: `&c` is a copy of the subclass struct
539
+ and does not contain the base's bytes, so a method cannot tell from it whether it
540
+ is holding the whole state or half of it.
541
+
542
+ ```pycon
543
+ >>> b = BoundedCounter()
544
+ >>> b.value, b.step, b.limit
545
+ (0, 1, 0)
546
+ >>> b.limit = 10
547
+ >>> b.bump(4)
548
+ 4
549
+ >>> b.increment() # a method of the base
550
+ 5
551
+ >>> repr(b)
552
+ 'BoundedCounter(value: 5, step: 1, limit: 10)'
553
+ >>> isinstance(b, Counter)
554
+ True
555
+ ```
556
+
557
+ ### Instance references
558
+
559
+ A parameter or result typed `&T`, where `T` is a `@[vc_class]`, is an **instance
560
+ reference**: the wrapper passes the instance itself rather than marshalling a
561
+ pointer across the boundary.
562
+
563
+ A `&T` parameter accepts an instance of `T` or of a subclass, and the V function
564
+ receives a pointer into that instance's live state, so it reads and writes the
565
+ same bytes a method of `T` would. Passing anything else raises `TypeError`:
566
+
567
+ <!-- readme-test: continue -->
568
+ ```v
569
+ // counter_value reads any counter, including a subclass instance.
570
+ @[vc_fn]
571
+ pub fn counter_value(c &Counter) int {
572
+ return c.value
573
+ }
574
+
575
+ // pick returns the instance the pointer came from.
576
+ @[vc_fn]
577
+ pub fn pick(first &Counter, second &Counter) &Counter {
578
+ return first
579
+ }
580
+ ```
581
+
582
+ A `&T` result is handed back to Python as an instance, not a pointer. The
583
+ returned pointer has to point into an instance the call already has — the
584
+ receiver of a method, or one of its `&T` arguments — so the wrapper can match it
585
+ and give Python the object that owns it:
586
+
587
+ <!-- readme-test: continue -->
588
+ ```v
589
+ // self_ref returns the receiver itself.
590
+ @[vc_method]
591
+ pub fn (c &Counter) self_ref() &Counter {
592
+ return c
593
+ }
594
+ ```
595
+
596
+ A `&T` result that points anywhere else raises `RuntimeError`: vcraft will not
597
+ guess at an object it does not own. A plain function (no receiver) that returns
598
+ `&T` therefore needs at least one `&T` parameter, and that is a compile-time
599
+ diagnostic rather than a runtime surprise.
600
+
601
+ ### Comparisons, sequence protocols and classmethods
602
+
603
+ `@[vc_eq]` and `@[vc_hash]` are a value comparison and its hash; `@[vc_lt]`, `@[vc_le]`,
604
+ `@[vc_gt]` and `@[vc_ge]` add ordering, so `<`, `sorted()` and `heapq` work. Each is a
605
+ free function over two state pointers, because V allows one receiver per method and an
606
+ operator needs two operands:
607
+
608
+ <!-- readme-test: continue -->
609
+ ```v
610
+ @[vc_lt]
611
+ pub fn counter_lt(a voidptr, b voidptr) bool {
612
+ mut x := Counter{}
613
+ mut y := Counter{}
614
+ vcraft.load_state(a, voidptr(&x), sizeof(Counter))
615
+ vcraft.load_state(b, voidptr(&y), sizeof(Counter))
616
+ return x.value < y.value
617
+ }
618
+
619
+ @[vc_ge]
620
+ pub fn counter_ge(a voidptr, b voidptr) bool {
621
+ mut x := Counter{}
622
+ mut y := Counter{}
623
+ vcraft.load_state(a, voidptr(&x), sizeof(Counter))
624
+ vcraft.load_state(b, voidptr(&y), sizeof(Counter))
625
+ return x.value >= y.value
626
+ }
627
+ ```
628
+
629
+ `@[vc_len]`, `@[vc_getitem]` and `@[vc_contains]` fill the sequence slots, so
630
+ `len(instance)`, `bool(instance)`, `instance[i]` and `x in instance` reach ordinary
631
+ methods. `@[vc_classmethod]` is registered like a static method but CPython passes the
632
+ class, so a subclass calling it sees itself. `@[vc_const]` exposes a `pub const` literal
633
+ as a module attribute:
634
+
635
+ ```v
636
+ @[vc_class]
637
+ pub struct Range {
638
+ mut:
639
+ @[vc_field] n int
640
+ }
641
+
642
+ @[vc_fn]
643
+ pub fn new_range(n int) &Range {
644
+ return &Range{ n: n }
645
+ }
646
+
647
+ @[vc_method]
648
+ @[vc_len]
649
+ pub fn (r &Range) size() int {
650
+ return r.n
651
+ }
652
+
653
+ @[vc_method]
654
+ @[vc_getitem]
655
+ pub fn (r &Range) at(i int) !int {
656
+ if i < 0 || i >= r.n {
657
+ return vcraft.raise_domain(.index_error, 'Range index out of range')
658
+ }
659
+ return i
660
+ }
661
+
662
+ @[vc_method]
663
+ @[vc_contains]
664
+ pub fn (r &Range) has(x int) bool {
665
+ return x >= 0 && x < r.n
666
+ }
667
+
668
+ @[vc_classmethod]
669
+ pub fn Range.starting_at(n int) int {
670
+ return n
671
+ }
672
+
673
+ @[vc_const]
674
+ pub const default_range = 8
675
+ ```
676
+
677
+ ### Numeric operators
678
+
679
+ `@[vc_add]`, `@[vc_sub]`, `@[vc_mul]`, `@[vc_truediv]`, `@[vc_floordiv]`, `@[vc_mod]`,
680
+ `@[vc_pow]`, `@[vc_lshift]`, `@[vc_rshift]`, `@[vc_and]`, `@[vc_or]` and `@[vc_xor]`
681
+ fill CPython's number slots, so `+`, `-`, `*`, `/`, `//`, `%`, `**`, `<<`, `>>`, `&`,
682
+ `|` and `^` work; `@[vc_neg]`, `@[vc_pos]`, `@[vc_abs]` and `@[vc_invert]` are the unary
683
+ ones. Each is a free function `(self &T, operand) -> result`, where the operand is a
684
+ scalar or another `&T` and the result is a number or a new `T`, which vcraft allocates
685
+ and fills:
686
+
687
+ ```v
688
+ @[vc_class]
689
+ pub struct Account {
690
+ mut:
691
+ @[vc_field] balance int
692
+ }
693
+
694
+ @[vc_fn]
695
+ pub fn new_account(balance int) &Account {
696
+ return &Account{ balance: balance }
697
+ }
698
+
699
+ @[vc_add]
700
+ pub fn account_add(a &Account, n int) Account {
701
+ return Account{ balance: a.balance + n }
702
+ }
703
+
704
+ @[vc_sub]
705
+ pub fn account_sub(a &Account, b &Account) int {
706
+ return a.balance - b.balance
707
+ }
708
+
709
+ @[vc_neg]
710
+ pub fn account_neg(a &Account) Account {
711
+ return Account{ balance: -a.balance }
712
+ }
713
+ ```
714
+
715
+ A scalar on the *left* (`2 + account`) reaches the reflected function. Write
716
+ `@[vc_radd]`, `@[vc_rsub]` and the rest with the same shape; for the commutative
717
+ operators (`add`, `mul`, `and`, `or`, `xor`) the forward function answers the reflected
718
+ call when no reflected one is given. An operand the function does not accept leaves the
719
+ slot returning `NotImplemented`, so Python raises its own `TypeError` rather than vcraft
720
+ guessing at a conversion.
721
+
722
+ A V function can call back into Python with `vcraft.call(callee, args)` (borrowed
723
+ arguments, a new reference back) or `vcraft.call_owned(callee, args)` (it releases the
724
+ arguments), and `vcraft.getattr` / `vcraft.call_method` reach an object's methods. The
725
+ callable usually arrives as a `voidptr` parameter:
726
+
727
+ <!-- readme-test: continue -->
728
+ ```v
729
+ @[vc_fn]
730
+ pub fn apply(f voidptr, x int) !int {
731
+ args := [vcraft.to_py_int(i64(x))]
732
+ r := vcraft.call_owned(f, args)
733
+ if vcraft.error_is_set() {
734
+ return error('callback failed')
735
+ }
736
+ out := vcraft.from_py_int(r, 'result')!
737
+ r.decref()
738
+ return out
739
+ }
740
+ ```
741
+
742
+ ### Reference fields and cycles
743
+
744
+ `@[vc_ref(Name)]` exposes a field as a strong reference to another instance. The field
745
+ is declared `vcraft.PyObj`, because that is what it holds: a pointer and a reference
746
+ count, and nothing V's collector would recognise.
747
+
748
+ ```v
749
+ @[vc_class]
750
+ pub struct Node {
751
+ mut:
752
+ @[vc_field] label int
753
+ @[vc_ref(Node)] peer vcraft.PyObj
754
+ }
755
+
756
+ // link makes two nodes point at each other.
757
+ //
758
+ // `retain`, not `steal`: a function parameter is borrowed, and the field has to keep the
759
+ // object alive on its own.
760
+ @[vc_method]
761
+ pub fn (mut n Node) link(other voidptr) {
762
+ n.peer = vcraft.retain(other)
763
+ }
764
+ ```
765
+
766
+ A class holding references is created with `Py_TPFLAGS_HAVE_GC` and gets `tp_traverse`
767
+ and `tp_clear`, so a cycle of instances is collected rather than leaked. Two details are
768
+ load-bearing and neither is obvious.
769
+
770
+ `Py_TPFLAGS_HAVE_GC` makes the *type object itself* collectable, so CPython calls
771
+ `tp_traverse` and `tp_clear` on the type as well as on its instances. A type's bytes after
772
+ the header are its dict, not V state, so both trampolines check whether they were
773
+ handed a type and hand that case to CPython before reading anything.
774
+
775
+ `PyObject_GC_UnTrack` reads a collector header that only exists on a collectable
776
+ allocation, so the deallocator's untrack is guarded on the flag. A class with no reference
777
+ fields does not set it, and untracking its instances unconditionally crashes several
778
+ collections later, inside `PyObject_GC_UnTrack`, with nothing in the frame pointing back.
779
+
780
+ `__repr__` is guarded with `Py_ReprEnter` for the same reason a cycle needs collecting in
781
+ the first place: two nodes that point at each other would otherwise recurse until the C
782
+ stack ran out, and the segfault would name neither.
783
+
784
+ The reference count is maintained by hand, because a `PyObj` is deliberately not a V
785
+ reference type. `retain` takes a reference of your own on a borrowed pointer, which is
786
+ what a method parameter is; `set_ref` counts one when Python assigns through the property;
787
+ `clear_ref` gives it back in `tp_clear` and `tp_dealloc`. A property setter's value is
788
+ borrowed, so the field counts its own rather than adopting CPython's.
789
+
790
+ Weak references are not supported yet: that needs a `tp_weaklistoffset` inside the
791
+ instance and registration in `tp_traverse`.
792
+
793
+ ### Persistent native buffers
794
+
795
+ An `@[vc_buffer]` field is a slice of scalars that lives for the object's lifetime, is
796
+ not copied on each call, and is freed exactly once at dealloc. It is how numeric state
797
+ stays in V across many calls instead of crossing the boundary per element: a stabilizer
798
+ tableau, a state vector, a scratch workspace.
799
+
800
+ ```v
801
+ // A vector of doubles held in a native buffer.
802
+ //
803
+ // The backing is allocated by `buffer_new_f64` in the constructor and freed by the
804
+ // generated `tp_dealloc`. `memoryview(v)` exposes the first buffer of the class with no
805
+ // copy; the others, if any, are reached through methods.
806
+ @[vc_class]
807
+ pub struct Vector {
808
+ mut:
809
+ @[vc_buffer] data []f64
810
+ }
811
+
812
+ // new_vector allocates a zeroed buffer of `n` doubles. The constructor takes an
813
+ // argument, which `tp_new` binds and passes here.
814
+ @[vc_fn]
815
+ pub fn new_vector(n int) &Vector {
816
+ return &Vector{
817
+ data: vcraft.buffer_new_f64(n)
818
+ }
819
+ }
820
+
821
+ // set stores a value at `i`, ignoring an out-of-range index.
822
+ @[vc_method]
823
+ pub fn (mut v Vector) set(i int, value f64) {
824
+ if i >= 0 && i < v.data.len {
825
+ v.data[i] = value
826
+ }
827
+ }
828
+
829
+ // total sums the buffer in V, with no per-element crossing of the boundary.
830
+ @[vc_method]
831
+ pub fn (v &Vector) total() f64 {
832
+ mut sum := 0.0
833
+ for x in v.data {
834
+ sum += x
835
+ }
836
+ return sum
837
+ }
838
+
839
+ // resize replaces the buffer with `n` zeroed doubles, refusing while a memoryview is open.
840
+ @[vc_method]
841
+ pub fn (mut v Vector) resize(n int) ! {
842
+ v.data = vcraft.buffer_resize_f64(v.data, n)!
843
+ }
844
+ ```
845
+
846
+ ```pycon
847
+ >>> v = m.Vector(4) # calls new_vector(4)
848
+ >>> v.set(0, 1.5); v.set(1, 2.5)
849
+ >>> v.total()
850
+ 4.0
851
+ >>> mv = memoryview(v) # zero-copy; writes go both ways
852
+ >>> mv.cast('d')[2] = 9.0
853
+ >>> v.total()
854
+ 13.0
855
+ >>> v.resize(2) # refused while the view is alive
856
+ BufferError: cannot resize a buffer while a memoryview of it is open
857
+ ```
858
+
859
+ The backing is allocated with the CPython allocator, not V's, because the state is
860
+ CPython-owned memory that V's collector does not scan: a V-heap pointer kept there would
861
+ look unreachable and be reclaimed while Python still held the object. It is therefore
862
+ owned by hand, and the only values an `@[vc_buffer]` field may hold are the ones the
863
+ `buffer_new_*` helpers return; assigning an ordinary V array to it would hand `buffer_free`
864
+ a pointer it does not own. The first `@[vc_buffer]` field of a class is the one
865
+ `memoryview(instance)` exposes; a class with more than one gets a warning, and the rest are
866
+ read and written through methods. A class that only inherits a buffer is covered by the
867
+ base's slot.
868
+
869
+ That `memoryview` is a 1-D *byte* view by default (`format "B"`, `itemsize 1`, length in
870
+ bytes). A `@[vc_buffer_typed]` beside `@[vc_buffer]` opts into a typed view instead: one
871
+ item per element, with the element's own format and width. A `[]f64` then reports
872
+ `format "d"`, `itemsize 8` and a length in elements, so `list(memoryview(x))` and
873
+ `struct.unpack` read the values directly. Both annotations go in one block on the field's
874
+ own line:
875
+
876
+ ```v
877
+ @[vc_class]
878
+ pub struct Samples {
879
+ mut:
880
+ @[vc_buffer; vc_buffer_typed] samples []f64
881
+ }
882
+ ```
883
+
884
+ A resize refuses with `BufferError` while a `memoryview` is open, because reallocating
885
+ moves the memory a live view still points at. A class that holds both an `@[vc_buffer]` and
886
+ an `@[vc_ref]` is collectable and owns native memory: `tp_dealloc` releases the references
887
+ and frees the buffers before the object goes.
888
+
889
+ ### Zero-copy results with `@[vc_memoryview]`
890
+
891
+ `@[vc_buffer]` keeps native state *inside* an object; `@[vc_memoryview]` is its mirror
892
+ image, applied to a function that *returns* a slice. Instead of building a Python list and
893
+ boxing every element, the result crosses as a writable `memoryview` over the same memory,
894
+ and the view owns the backing: it is freed when Python collects the view, through the same
895
+ allocator. The function must therefore return a slice built by `buffer_new_*` or
896
+ `buffer_resize_*`; an ordinary V array lives on V's heap and must never be handed over.
897
+
898
+ ```v
899
+ // A ramp of doubles, returned as a writable memoryview.
900
+ //
901
+ // The backing comes from `buffer_new_f64`, so the memoryview owns it and frees it when
902
+ // the view is collected. A plain `[]f64` returned here would be freed by the wrong
903
+ // allocator and crash.
904
+ @[vc_fn]
905
+ @[vc_memoryview]
906
+ pub fn ramp(n int) []f64 {
907
+ mut out := vcraft.buffer_new_f64(n)
908
+ for i in 0 .. n {
909
+ out[i] = f64(i)
910
+ }
911
+ return out
912
+ }
913
+ ```
914
+
915
+ ```pycon
916
+ >>> mv = m.ramp(4)
917
+ >>> mv.format, mv.itemsize, mv.readonly
918
+ ('d', 8, False) # writable, so writes go both ways
919
+ >>> del mv # frees the native backing
920
+ ```
921
+
922
+ The stub says `memoryview`. `@[vc_memoryview]` combines with `@[vc_fn]` or `@[vc_method]`
923
+ exactly as `@[vc_nogil]` does, and is refused on a `@[vc_raw]` function (raw already hands
924
+ you the `PyObject`). The element type must be a buffer scalar -- `bool`, an integer or a
925
+ float -- and the return type a slice or `[]u8`-like sequence of one.
926
+
927
+ For state that is not a plain slice -- a handle from a C library, a struct full of
928
+ pointers -- `vcraft.new_handle(value, destructor)` wraps a native pointer in an anonymous
929
+ `PyCapsule`, and `vcraft.handle_data(capsule)` recovers it. The capsule calls `destructor`
930
+ when Python collects it; CPython passes the destructor the capsule object, so call
931
+ `handle_data` there to get the pointer back:
932
+
933
+ ```v
934
+ // new_box allocates an int behind a capsule that frees it.
935
+ @[vc_fn]
936
+ pub fn new_box(value int) vcraft.PyObj {
937
+ p := vcraft.native_alloc(usize(sizeof(int)))
938
+ if p == unsafe { nil } {
939
+ panic('memory allocation failure')
940
+ }
941
+ unsafe {
942
+ *(&int(p)) = value
943
+ }
944
+ return vcraft.new_handle(p, voidptr(box_free))
945
+ }
946
+
947
+ fn box_free(capsule voidptr) {
948
+ vcraft.native_free(vcraft.handle_data(capsule))
949
+ }
950
+ ```
951
+
952
+ This is the general escape hatch next to `@[vc_raw]`: a `@[vc_raw]` function can take the
953
+ capsule as an opaque `voidptr` and call `vcraft.handle_data` on it.
954
+
955
+ ### Custom error types
956
+
957
+ `@[vc_error]` marks a struct as the error of a `!T` function. V requires `msg()` and
958
+ `code()`, and the generator reports the annotation on a struct that has not got both rather
959
+ than letting it fail at the V compiler with a message about an interface.
960
+
961
+ There are three ways for such an error to name its Python exception, and they are not
962
+ equivalent.
963
+
964
+ **`code()` is the exception.** The enum's own numbering is the channel, so nothing else is
965
+ needed:
966
+
967
+ ```v
968
+ @[vc_error]
969
+ pub struct ConfigError {
970
+ pub:
971
+ detail string
972
+ line int
973
+ }
974
+
975
+ pub fn (e ConfigError) msg() string {
976
+ return 'line ${e.line}: ${e.detail}'
977
+ }
978
+
979
+ // The code *is* the exception: `PyExc.value_error` is 2.
980
+ pub fn (e ConfigError) code() int {
981
+ return int(vcraft.PyExc(.value_error))
982
+ }
983
+
984
+ @[vc_fn]
985
+ pub fn load(text string) !string {
986
+ // ...
987
+ return ConfigError{ detail: 'not a name=value line', line: 1 }
988
+ }
989
+ ```
990
+
991
+ A code outside the enum is an ordinary error code rather than an exception choice, and
992
+ those are common, so anything unrecognised becomes a `RuntimeError`. `code() == 0`, which
993
+ is every anonymous `error('...')`, is a `RuntimeError` too: `0` is `PyExc.none`, whose
994
+ object is the null pointer, and `PyErr_SetString` writes through it rather than checking.
995
+
996
+ **`raise_custom` sets an arbitrary class.** For an exception vcraft has no name for,
997
+ including one the caller defined in Python:
998
+
999
+ ```v
1000
+ @[vc_fn]
1001
+ pub fn parse(text string, missing voidptr) !int {
1002
+ if text.len == 0 || !text.bytes().all(it.is_digit()) {
1003
+ return vcraft.raise_custom(missing, 'not a number')
1004
+ }
1005
+ return text.int()
1006
+ }
1007
+ ```
1008
+
1009
+ **A `PyObj` field plus the generated raiser.** To keep the class in the error value rather
1010
+ than passing it around, one `PyObj` field and the raiser the generator emits:
1011
+
1012
+ ```v
1013
+ @[vc_error]
1014
+ pub struct Rejected {
1015
+ pub:
1016
+ exc vcraft.PyObj
1017
+ detail string
1018
+ }
1019
+
1020
+ pub fn (e Rejected) msg() string { return e.detail }
1021
+ pub fn (e Rejected) code() int { return 0 }
1022
+
1023
+ // generated:
1024
+ // fn vcraft_generated__raise_rejected(e Rejected) IError {
1025
+ // return vcraft.raise_custom(e.exc.ptr, e.msg())
1026
+ // }
1027
+
1028
+ @[vc_fn]
1029
+ pub fn checked(value int, exc voidptr) !int {
1030
+ if value < 0 {
1031
+ return vcraft_generated__raise_rejected(Rejected{
1032
+ exc: vcraft.borrow(exc)
1033
+ detail: 'value must not be negative'
1034
+ })
1035
+ }
1036
+ return value * 2
1037
+ }
1038
+ ```
1039
+
1040
+ Two `PyObj` fields are reported rather than guessed at, since there is no way to tell which
1041
+ one is the class.
1042
+
1043
+ What none of these can do is carry the choice *inside* the error and have the wrapper read
1044
+ it there: V erases an error to `IError` by the time the wrapper sees it, and an `IError`
1045
+ has a message and nothing else. That is why `raise_domain` sets the exception at the point
1046
+ of failure, and why the raiser exists.
1047
+
1048
+
1049
+
1050
+ ### Buffers without copying
1051
+
1052
+ A `[]u8` parameter accepts anything bytes-like through the buffer protocol:
1053
+
1054
+ ```v
1055
+ // checksum adds every byte it is given.
1056
+ //
1057
+ // `bytes`, `bytearray`, `memoryview`: nothing is copied on the way in.
1058
+ @[vc_fn]
1059
+ pub fn checksum(data []u8) int {
1060
+ mut total := 0
1061
+ for b in data {
1062
+ total = (total + int(b)) & 0xffffff
1063
+ }
1064
+ return total
1065
+ }
1066
+ ```
1067
+
1068
+ The wrapper acquires a view, aliases it as a V slice for exactly the call, and
1069
+ releases it on the way out -- including the error paths, which is why the release
1070
+ is deferred rather than written after the call. The slice must not outlive the
1071
+ call: the release drops the exporter's reference, and a stored slice would point
1072
+ at memory nobody owns. Returns copy the other way, because an immutable Python
1073
+ `bytes` cannot alias V memory.
1074
+
1075
+ ### Iterators
1076
+
1077
+ `@[vc_iter]` and `@[vc_next]` always come as a pair: an iterator that cannot produce
1078
+ items fails at the first `next()`, and items without an iterator are unreachable,
1079
+ so a half pair is reported rather than emitted.
1080
+
1081
+ ```v
1082
+ @[vc_class]
1083
+ pub struct Countdown {
1084
+ mut:
1085
+ @[vc_field] current int
1086
+ @[vc_field] start int
1087
+ }
1088
+
1089
+ // rewind resets the countdown, so the same instance can be iterated twice.
1090
+ @[vc_method]
1091
+ @[vc_iter]
1092
+ pub fn (mut c Countdown) rewind() {
1093
+ c.current = c.start
1094
+ }
1095
+
1096
+ // next yields the current value and steps down, refusing past zero.
1097
+ @[vc_method]
1098
+ @[vc_next]
1099
+ pub fn (mut c Countdown) advance() !int {
1100
+ if c.current <= 0 {
1101
+ return vcraft.raise_domain(.stop_iteration, 'no more values')
1102
+ }
1103
+ c.current--
1104
+ return c.current + 1
1105
+ }
1106
+ ```
1107
+
1108
+ ```pycon
1109
+ >>> c = Countdown()
1110
+ >>> c.start = 3
1111
+ >>> c.current = 3
1112
+ >>> list(c)
1113
+ [3, 2, 1]
1114
+ ```
1115
+
1116
+ The instance is its own iterator: `@[vc_iter]` runs for its side effects and the
1117
+ slot returns the instance itself. `@[vc_next]` produces one item per call, and a V
1118
+ error ends the iteration -- cleanly for `StopIteration`, loudly for anything else,
1119
+ which is CPython's own contract for the slot rather than something vcraft
1120
+ invented.
1121
+
1122
+ ---
1123
+
1124
+ ## Errors and panics
1125
+
1126
+ A `!T` return becomes a Python exception:
1127
+
1128
+ ```v
1129
+ // Greet someone by name.
1130
+ @[vc_fn]
1131
+ pub fn greet(name string) !string {
1132
+ if name.len == 0 {
1133
+ return error('name must not be empty')
1134
+ }
1135
+ return 'Hello, ${name}!'
1136
+ }
1137
+ ```
1138
+
1139
+ ```python
1140
+ >>> m.greet('')
1141
+ RuntimeError: name must not be empty
1142
+ ```
1143
+
1144
+ A bare `error('...')` carries nothing but a message, so it becomes a
1145
+ `RuntimeError`. That is right often enough to hide the problem: Python code that
1146
+ divides by zero expects `ZeroDivisionError`, and an `except ZeroDivisionError`
1147
+ around a call into a V extension silently stops matching. So a failure that means a
1148
+ particular exception says so:
1149
+
1150
+ ```v
1151
+ // Divides two floats, refusing a zero divisor.
1152
+ @[vc_fn]
1153
+ pub fn divide(a f64, b f64) !f64 {
1154
+ if b == 0.0 {
1155
+ return vcraft.raise_domain(.zero_division_error, 'division by zero')
1156
+ }
1157
+ return a / b
1158
+ }
1159
+ ```
1160
+
1161
+ ```python
1162
+ >>> m.divide(1.0, 0.0)
1163
+ ZeroDivisionError: division by zero
1164
+ ```
1165
+
1166
+ The exception is chosen where the failure happens rather than in the wrapper,
1167
+ because V gives a wrapper nothing to choose from: every error arrives as an
1168
+ `IError` holding a message, and a bare message does not say whether it was a
1169
+ `ValueError` or a `ZeroDivisionError`. `raise_domain` sets the Python exception and
1170
+ returns an error, and the wrapper's `error_is_set` check stops the value from
1171
+ reaching Python as a result. A wrapper never overwrites an exception that is already
1172
+ set, so the choice survives.
1173
+
1174
+ A custom V error struct cannot carry the choice. V 0.5.2 rejects a value whose type
1175
+ is not `IError`, and a struct that implements `msg()` is not one, so the exception
1176
+ travels out of band through the pending Python exception instead of inside the error
1177
+ value.
1178
+
1179
+ Argument marshalling failures are separate and never reach your code: a wrong
1180
+ argument type is a `TypeError` and the wrong number of arguments is a `TypeError`,
1181
+ both naming the parameter.
1182
+
1183
+ ### Panics
1184
+
1185
+ A V `panic` prints a message and calls `exit(1)`, which inside CPython would take
1186
+ the whole interpreter down with it. V 0.5 has Go-style recovery, so every generated
1187
+ wrapper installs a frame:
1188
+
1189
+ <!-- readme-test: skip -->
1190
+ ```v
1191
+ fn _vcraft_generated__wrap_first_char(text string) string {
1192
+ defer {
1193
+ if message := recover() {
1194
+ vcraft.raise_runtime_error('panic in V code: ${message}')
1195
+ }
1196
+ }
1197
+ return first_char(text)
1198
+ }
1199
+ ```
1200
+
1201
+ ```python
1202
+ >>> m.first_char('')
1203
+ RuntimeError: panic in V code: substr(0, 1) out of bounds (len=0) s=
1204
+ >>> m.add(2, 3) # the interpreter is still fine
1205
+ 5
1206
+ ```
1207
+
1208
+ A recovered panic is raised rather than reaching Python as a half-written result,
1209
+ which is what the wrapper's `error_is_set` check after the call is for. Panic state
1210
+ is thread-local in V, so this stays correct under free threading. A method works on
1211
+ the instance in place, as a PyO3 method does, so one that panics halfway keeps the
1212
+ fields it had already written.
1213
+
1214
+ The guard is inlined per trampoline rather than shared through a helper. V emits no
1215
+ forward declaration for a generic function called across modules, so a shared
1216
+ wrapper fails to compile with an implicit-declaration error.
1217
+
1218
+ `@[vc_nogil]` marks a function as pure V with no Python interaction. The GIL is
1219
+ released around the call, so long-running V code runs in parallel the way
1220
+ `py.allow_threads` does in PyO3:
1221
+
1222
+ ```v
1223
+ // spin burns time in pure V, so threads can prove the GIL is really released.
1224
+ //
1225
+ // Nothing in here touches Python, raises, or allocates in a way the collector would
1226
+ // need the interpreter for.
1227
+ @[vc_fn]
1228
+ @[vc_nogil]
1229
+ pub fn spin(iterations int) int {
1230
+ mut total := 0
1231
+ for i in 0 .. iterations {
1232
+ total = (total + i * 7) & 0x7fffffff
1233
+ }
1234
+ return total
1235
+ }
1236
+ ```
1237
+
1238
+ ```pycon
1239
+ >>> import threading
1240
+ >>> threads = [threading.Thread(target=lambda: spin(3000000)) for _ in range(4)]
1241
+ >>> [t.start() for t in threads]
1242
+ >>> [t.join() for t in threads] # each burns its own core
1243
+ ```
1244
+
1245
+ The annotation is a promise, not a hint, and the generator cannot verify purity. No
1246
+ Python calls, no `raise_domain`, no touching a `PyObj` while released:
1247
+ `raise_domain` sets a Python exception, which without the GIL corrupts the
1248
+ interpreter state rather than reporting anything. A `!T` function fails with a plain
1249
+ `error(...)` instead, and the wrapper turns it into an exception after it holds the
1250
+ GIL again. `@[vc_nogil]` on a `@[vc_raw]` function is refused outright: raw means the
1251
+ function handles `PyObject *` itself, which is the opposite of pure.
1252
+
1253
+ The pairing is exact on every path -- success, `!T` failure, and panic -- because an
1254
+ unbalanced release corrupts the GIL count and crashes the next thread switch inside
1255
+ CPython rather than anywhere near the call. And the per-thread state chain (see [reference fields and cycles](#reference-fields-and-cycles)) is what keeps two threads in two
1256
+ trampolines from publishing into each other's slots: with the GIL held one global
1257
+ would do, and without it each thread needs its own.
1258
+
1259
+ ---
1260
+
1261
+ ## The command line
1262
+
1263
+ ```
1264
+ vcraft new <name> scaffold a V project ready for Python
1265
+ vcraft build [options] build a distributable wheel
1266
+ vcraft develop [options] build and install an editable pointer by default
1267
+ vcraft develop --copy install a plain extension copy instead
1268
+ vcraft sdist build a source distribution
1269
+ vcraft generate-ci emit a GitHub Actions workflow into .github/workflows/
1270
+ vcraft publish upload the built distributions to PyPI
1271
+ vcraft info show what vcraft resolved for this project
1272
+ vcraft clean remove build output
1273
+ vcraft version print the version
1274
+
1275
+ build options:
1276
+ --release compile with -prod
1277
+ --abi3 <version> one wheel usable from CPython <version> onwards (3.11+)
1278
+ --interpreter <path> build against a specific interpreter
1279
+ --out-dir <dir> output directory (default: dist/)
1280
+ --platform <tag> override the platform tag
1281
+ --target <name> cross-compile for a target, e.g. linux-aarch64-gnu
1282
+ --manylinux <version> claim a manylinux policy, e.g. 2_17
1283
+ --musllinux <version> claim a musllinux policy, e.g. 1_2
1284
+ --cc <compiler> C compiler for V to invoke
1285
+ --cflags <flags> extra flags for the C compiler
1286
+ --ldflags <flags> extra flags for the C linker
1287
+ --dry-run print the build plan without building
1288
+ --free-threading build against a free-threaded interpreter (3.13t+)
1289
+ --strip strip symbols
1290
+ --skip-audit do not validate the resulting wheel
1291
+ --jobs <n> compiler parallelism
1292
+ --editable point the environment at this build instead of copying
1293
+ --copy install a copy, which is the opposite of --editable
1294
+ ```
1295
+
1296
+ `vcraft` never writes to a system location and never installs anything outside
1297
+ the project. The V runtime module is located relative to the `vcraft` binary
1298
+ itself and passed to the compiler with `-path`, so `VMODULES` is left untouched.
1299
+
1300
+ A project from scratch, end to end:
1301
+
1302
+ ```console
1303
+ $ ./scripts/build-vcraft.sh
1304
+ $ vcraft new mypkg
1305
+ $ cd mypkg
1306
+ $ python -m venv .venv && source .venv/bin/activate
1307
+ $ vcraft develop
1308
+ $ python -c "import mypkg as m; print(m.greet('world'))"
1309
+ Hello, world!
1310
+ ```
1311
+
1312
+ Packaging configuration lives in `vcraft.toml`, not in `v.mod`. `v.mod` is V's
1313
+ manifest and names the V module; a wheel also needs a distribution name, a version,
1314
+ a licence and a `Requires-Python`, and none of those are V's business.
1315
+
1316
+ See [`vlib/vcraft_project/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/vlib/vcraft_project/README.md) for the TOML
1317
+ subset that is parsed, and for the compiler constraints that shaped it.
1318
+
1319
+ ---
1320
+
1321
+ ## Wheels
1322
+
1323
+ ```console
1324
+ $ vcraft build --release
1325
+ $ pip install dist/my_extension-0.1.0-cp314-cp314-linux_x86_64.whl
1326
+ ```
1327
+
1328
+ A wheel is written from scratch in V: DEFLATE, the ZIP container, `METADATA`, `WHEEL`,
1329
+ `RECORD` with SHA-256, the compatibility tag, and the file name.
1330
+
1331
+ zlib is not linked. It is on most build hosts but not all, and linking it makes the
1332
+ extension depend on a shared library the manylinux and musllinux images have to agree
1333
+ on. About 250 lines of fixed-Huffman DEFLATE is cheaper than that or than vendoring
1334
+ zlib, and the output is read by every unzip. A `.so` compresses to roughly 43%.
1335
+
1336
+ `RECORD` carries base64 digests with the URL-safe alphabet and no padding, because pip
1337
+ compares the string. A hex digest installs and then fails verification on every file.
1338
+
1339
+ The details that are easy to get wrong are written up in
1340
+ [`vlib/vcraft_wheel/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/vlib/vcraft_wheel/README.md). The shortest version:
1341
+
1342
+ - The distribution name is **escaped**, a run of `-_.` becoming `_`. The version is
1343
+ escaped too, but only its dashes: `-` becomes `_` and `.` stays `.`. Writing `0-1-0`
1344
+ looks equivalent because PEP 440 agrees it is the same version, but the installer
1345
+ splits the file name on `-` and pip rejects it with "wrong number of parts".
1346
+ - The `.dist-info` directory is named after the distribution, never the module. pip
1347
+ checks this before opening the archive.
1348
+ - The extension goes at the archive root. A directory named after the module is a
1349
+ namespace package, and Python resolves one without opening the files inside it, so
1350
+ the wheel installs and then imports as an empty module with `__file__` of `None`.
1351
+
1352
+ Hand-written Python under a project's `python/` directory is packaged under the same
1353
+ relative names; `embed-pyc = true` ships sourceless `.pyc` files instead. `vcraft
1354
+ develop` is editable by default: it installs a `.pth` pointing at the build output and
1355
+ the project's Python sources, plus distribution metadata, while `develop --copy`
1356
+ installs a plain extension copy. `pip install -e .` uses the generated PEP 660 backend.
1357
+ Editable wheels point at the local build tree and are refused by `vcraft publish`.
1358
+
1359
+ ### Cross-compilation targets
1360
+
1361
+ `--target` names the machine a wheel is built for rather than the one building it.
1362
+ The canonical form is `<os>-<arch>-<libc>` -- `linux-aarch64-gnu`,
1363
+ `linux-x86_64-musl`, `macos-arm64`, `windows-amd64` -- and Rust-style triples
1364
+ like `aarch64-unknown-linux-gnu` are accepted as aliases. There is deliberately
1365
+ no `universal2`: a fat binary needs two architectures linked together and one
1366
+ build makes one, so the tag always names the architecture actually built. A Linux
1367
+ policy is claimed separately and explicitly, because the tag is a statement about
1368
+ the libc the binary was linked against:
1369
+
1370
+ ```console
1371
+ $ vcraft build --target linux-aarch64-gnu --manylinux 2_17
1372
+ $ vcraft build --target linux-x86_64-musl --musllinux 1_2 --cc x86_64-linux-musl-gcc
1373
+ ```
1374
+
1375
+ A target without a policy keeps the plain `linux_<arch>` tag: claiming manylinux is
1376
+ a statement a build that has not verified its libc must not make. Naming the host's
1377
+ own target changes nothing -- the wheel is byte-identical to a default build, which
1378
+ is checked -- and a `--platform` that disagrees with the target is refused rather
1379
+ than warned about, because the wheel would lie about what it contains.
1380
+
1381
+ `--dry-run` prints the resolved OS, architecture, tag, extension suffix and compiler
1382
+ invocation without touching a toolchain. Planning is pure; compiling is not, so a
1383
+ target whose cross compiler is not installed verifies this far and fails with the
1384
+ compiler's name when asked to build for real. Foreign execution -- actually running
1385
+ an aarch64 or musl wheel -- happens in CI, where the images carry the toolchains.
1386
+
1387
+ ## Generated project layout
1388
+
1389
+ What `vcraft new my_extension` writes:
1390
+
1391
+ ```
1392
+ my_extension/
1393
+ ├── vcraft.toml packaging configuration (see Configuration)
1394
+ ├── v.mod Module { name: 'my_extension_native' }
1395
+ ├── pyproject.toml names the PEP 517/660 build backend
1396
+ ├── vcraft_build.py the backend, emitted by the vcraft that wrote it
1397
+ ├── main.py a runnable example; not packaged into the wheel
1398
+ ├── tests/
1399
+ │ ├── __init__.py
1400
+ │ └── test_my_extension.py unittest cases for the example
1401
+ ├── README.md .gitignore
1402
+ ├── python/
1403
+ │ └── my_extension/
1404
+ │ ├── __init__.py re-exports the native module
1405
+ │ └── py.typed
1406
+ └── src/
1407
+ └── my_extension_native.v your code
1408
+ ```
1409
+
1410
+ `vcraft new` writes a Python package named after the distribution that re-exports the
1411
+ native module, so `import my_extension` is the public name and `my_extension_native`
1412
+ stays an implementation detail. There is no configuration for it: hand-written Python
1413
+ under a project's `python/` directory is packaged under the same relative names, so a
1414
+ project can mix V and Python freely.
1415
+
1416
+ `vcraft develop` (and any other build) adds `src/_vcraft_generated.v` and
1417
+ `python/<module>.pyi`, both generated and git-ignored, installs the extension, and
1418
+ writes wheels to `dist/`. The `.pyi` stub travels in the wheel next to the extension,
1419
+ so a type checker sees the signatures `help()` shows. `vcraft generate-ci` adds
1420
+ `.github/workflows/build.yml`.
1421
+
1422
+ `main.py` and `tests/` are for the development loop and are not part of the
1423
+ distribution, though both travel in the sdist.
1424
+
1425
+ `pyproject.toml` is written once by `vcraft new` and then merged on every build: vcraft
1426
+ owns its `[build-system]` table and preserves everything else the project adds to the
1427
+ file.
1428
+
1429
+ ---
1430
+
1431
+ ## Continuous integration
1432
+
1433
+ `vcraft generate-ci` writes `.github/workflows/build.yml` for your project:
1434
+
1435
+ ```console
1436
+ $ vcraft generate-ci
1437
+ ```
1438
+
1439
+ The matrix is derived from `vcraft.toml`:
1440
+
1441
+ - **Regular builds:** one wheel per supported CPython from the project's
1442
+ `minimum-version` up, for manylinux x86_64 and aarch64 and for macOS arm64.
1443
+ - **`abi3`:** one wheel per platform covers every CPython from the floor up, plus
1444
+ musllinux x86_64 and aarch64.
1445
+ - **`free-threading`:** 3.13t, 3.14t and 3.15t wheels for Linux and macOS.
1446
+
1447
+ Linux wheels are built inside published images that already carry V and vcraft,
1448
+ `ghcr.io/rroblf01/vcraft-manylinux` (derived from PyPA's `manylinux_2_28`) and
1449
+ `ghcr.io/rroblf01/vcraft-musllinux` (Alpine), each on a native runner for its
1450
+ architecture. Every wheel is then installed and imported in the image or
1451
+ interpreter it was built for before the job passes, so a wheel that would fail on a
1452
+ user's machine fails in CI instead. See [`docker/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/docker/README.md) for
1453
+ how the images are built.
1454
+
1455
+ The build step uses the vcraft action, which mirrors `maturin-action`. On a runner
1456
+ it installs vcraft from PyPI and a cached V compiler; with `container:` it runs
1457
+ inside one of the images instead. Linux, macOS and Windows runners are supported.
1458
+
1459
+ ```yaml
1460
+ - uses: rroblf01/vcraft/actions/vcraft-action@v1
1461
+ with:
1462
+ vcraft-version: v1.4.0
1463
+ args: build --release
1464
+ ```
1465
+
1466
+ vcraft is also a PEP 517 build backend (`pip install .` works), so other
1467
+ wheel-building tools can drive it, but only the workflow above is tested.
1468
+
1469
+ ---
1470
+
1471
+ ## Configuration
1472
+
1473
+ Packaging configuration lives in `vcraft.toml` at the project root. `vcraft new`
1474
+ writes one. `pyproject.toml` is the only other file vcraft touches, and it is merged
1475
+ rather than replaced: a build rewrites just the `[build-system]` table and leaves every
1476
+ other table alone. A project's own `[project]`, `[dependency-groups]` (PEP 735, so
1477
+ `uv sync --group dev` works) and `[tool.*]` config survive `vcraft build` and
1478
+ `vcraft develop`.
1479
+
1480
+ ```toml
1481
+ [package]
1482
+ name = "my-extension"
1483
+ version = "0.1.0"
1484
+ module = "my_extension"
1485
+ description = "A Python extension written in V."
1486
+ license = "MIT"
1487
+ requires-python = ">=3.11"
1488
+ readme = "README.md" # the PyPI project page, rendered as Markdown
1489
+ keywords = ["fast", "parsing"]
1490
+ classifiers = ["Programming Language :: Other", "Programming Language :: Python :: 3"]
1491
+ dependencies = []
1492
+
1493
+ [build]
1494
+ minimum-version = "3.11" # oldest CPython the CI matrix builds a wheel for
1495
+ abi3 = "3.11" # optional: one stable-ABI wheel for 3.11 and newer
1496
+ free-threading = false # true: build for a free-threaded CPython (3.13t+)
1497
+ strip = false
1498
+ embed-pyc = false
1499
+ gc-free-space-divisor = 2 # Boehm heap growth: 1 favours speed, 2 memory
1500
+
1501
+ [urls]
1502
+ Source = "https://github.com/me/my-extension"
1503
+ Issues = "https://github.com/me/my-extension/issues"
1504
+ ```
1505
+
1506
+ Before 1.0 the build keys were top-level keys and classifiers were `[[classifier]]`
1507
+ tables; both are still read throughout 1.x, with a warning saying what to change.
1508
+
1509
+ The keys of `[build]`:
1510
+
1511
+ | Key | Default | Meaning |
1512
+ | ----------------- | --------- | -------------------------------------------------------------- |
1513
+ | `minimum-version` | `"3.11"` | Oldest CPython `vcraft generate-ci` builds a wheel for |
1514
+ | `abi3` | unset | Stable-ABI floor for a single portable wheel, `3.11` or newer |
1515
+ | `free-threading` | `false` | Build for a free-threaded interpreter and declare it GIL-free |
1516
+ | `strip` | `false` | Strip the extension |
1517
+ | `embed-pyc` | `false` | Ship sourceless `.pyc` files instead of the project's `.py` |
1518
+ | `gc-free-space-divisor` | `2` | Boehm heap growth divisor; `1` trades memory for speed |
1519
+
1520
+ In `[package]`, `readme` names the file PyPI shows as the project page (`.md` is
1521
+ rendered as Markdown, `.rst` as reStructuredText); `vcraft new` writes a
1522
+ `README.md` and points at it. Classifiers must be ones PyPI knows, or it rejects
1523
+ the upload: V has none of its own, so use `Programming Language :: Other`. Each
1524
+ entry of `[urls]` becomes a link in the PyPI sidebar.
1525
+
1526
+ `vcraft build` refuses the combinations that would compile and then fail later: an
1527
+ interpreter older than 3.11, an `abi3` floor below 3.11 or above the interpreter
1528
+ building it, `abi3` together with `free-threading` (free-threaded CPython has no
1529
+ stable ABI), and `free-threading` on anything older than 3.13.
1530
+
1531
+ ---
1532
+
1533
+ ## Performance
1534
+
1535
+ The same workloads implemented with PyO3, vcraft and zig-maturin, plus pure
1536
+ Python, measured on Linux x86_64 (AMD Ryzen 5 3600), CPython 3.13, vcraft 1.3.0.
1537
+ Time per call, lower is better:
1538
+
1539
+ | workload | PyO3 | vcraft | pure Python |
1540
+ |---|---|---|---|
1541
+ | `add(1, 2)`: call overhead | **71 ns** | 73 ns | 37 ns |
1542
+ | `greet(name)`: str in, new str out | 135 ns | **120 ns** | 73 ns |
1543
+ | `fib(25)`: pure compute | 237.3 µs | **112.0 µs** | 9.36 ms |
1544
+ | `sum_floats(100k)`: list[float] in | 881.1 µs | **223.3 µs** | 1.59 ms |
1545
+ | `Counter()`: object construction | 96 ns | **92 ns** | 97 ns |
1546
+ | `c.add(1)`: method call | **67 ns** | 68 ns | 59 ns |
1547
+ | `count_primes(1e6)`: compute + native alloc | **2.00 ms** | 2.22 ms | 98.07 ms |
1548
+
1549
+ The state a subclass inherits is published only by the trampolines that can
1550
+ observe it, so a class with no base pays nothing for it; on top of that, the
1551
+ instance state now lives inside the object, so vcraft constructs objects faster
1552
+ than PyO3 and its attributes and no-argument methods are ahead too. The vcraft
1553
+ wheel is the smallest of the three and its clean release build the fastest.
1554
+ Methodology, every workload, memory use and import time are in
1555
+ [`benchmark/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/benchmark/README.md) and
1556
+ [`benchmark/results-linux.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/benchmark/results-linux.md).
1557
+
1558
+ ---
1559
+
1560
+ ## Project status
1561
+
1562
+ **Stable since 1.0.** The annotation vocabulary, the `vcraft.toml` keys and the CLI
1563
+ change only with a deprecation period (see [Stability](#stability)); every change is
1564
+ listed in the [changelog](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/CHANGELOG.md).
1565
+
1566
+ What is tested on every commit:
1567
+
1568
+ - CPython 3.11, 3.12, 3.13, 3.14 and 3.15 on Linux x86_64, and 3.11 and 3.15 on macOS
1569
+ arm64, with the full suite: runtime, code generator, wheel writer, packaging and
1570
+ the CLI end to end.
1571
+ - Free-threaded CPython 3.13t, 3.14t and 3.15t on Linux and 3.15t on macOS, importing
1572
+ with the GIL disabled and calling from several threads.
1573
+
1574
+ Known limitations:
1575
+
1576
+ - **Windows is supported for `win_amd64`, and is new.** `vcraft build` links a
1577
+ `win_amd64` `.pyd` against the interpreter's own import library, the action and
1578
+ `vcraft generate-ci` run on `windows-latest`, and the core suites pass there. arm64
1579
+ Windows and MSVC are not supported yet.
1580
+ - **V has to be built from source** at the pinned commit until V publishes a release
1581
+ newer than 0.5.2.
1582
+ - **aarch64 Linux** wheels and images are built on native runners but are not part
1583
+ of the per-commit test matrix.
1584
+ - Extensions use single-phase initialisation, so a module is shared by all
1585
+ subinterpreters.
1586
+
1587
+ ## Built with vcraft
1588
+
1589
+ [vqubit](https://pypi.org/project/vqubit/) is a dependency-free quantum computing framework
1590
+ for Python in the spirit of [Cirq](https://quantumai.google/cirq): its linear algebra, gate
1591
+ matrices and simulators are written in V. It ships as a single abi3 wheel per platform,
1592
+ CPython 3.11 and newer, and pulls in nothing else. It is the reference for what a vcraft
1593
+ project looks like end to end — [source](https://github.com/rroblf01/vqubit),
1594
+ [docs](https://rroblf01.github.io/vqubit/).
1595
+
1596
+ ## Stability
1597
+
1598
+ vcraft follows [Semantic Versioning](https://semver.org/). From 1.0, the annotations,
1599
+ the `vcraft.toml` keys, the CLI commands and options, and the Python behaviour of the
1600
+ conversions in the type table are the public surface:
1601
+
1602
+ - A **minor** release (1.1, 1.2...) only adds to it: new annotations, keys, options and
1603
+ types.
1604
+ - A name that is replaced keeps working for the rest of 1.x, and every use of it draws
1605
+ a warning naming its replacement. It is removed in **2.0**, never before.
1606
+ - Generated glue, the runtime's V API under `vlib/vcraft`, and the wheel's internals
1607
+ are not part of the surface: they change whenever the generator needs them to.
1608
+
1609
+ Upgrading a 0.x project: [MIGRATING.md](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/MIGRATING.md). Reporting a vulnerability:
1610
+ [SECURITY.md](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/SECURITY.md).
1611
+
1612
+ Renamed in 1.0, still accepted with a warning: `@[vc_gil]` (now `@[vc_nogil]`),
1613
+ `@[vc_methods]` (now `@[vc_method]`), top-level build keys in `vcraft.toml` (now in
1614
+ `[build]`) and `[[classifier]]` tables (now `classifiers = [...]`).
1615
+
1616
+ ## Requirements
1617
+
1618
+ - Python 3.11 to 3.15, or 3.13t/3.14t/3.15t for free-threaded builds
1619
+ - Linux (glibc or musl) or macOS
1620
+ - A V compiler at the pinned commit: `vcraft toolchain install` builds it
1621
+ - gcc or clang, and the CPython development headers
1622
+ - Docker, only to build manylinux or musllinux wheels locally
1623
+
1624
+ ---
1625
+
1626
+ ## Development
1627
+
1628
+ ```console
1629
+ $ git clone https://github.com/rroblf01/vcraft && cd vcraft
1630
+ $ ./scripts/build-vcraft.sh # builds bin/vcraft
1631
+ ```
1632
+
1633
+ Each test suite is a standalone, standard-library-only script that builds what it
1634
+ needs:
1635
+
1636
+ ```console
1637
+ $ python3 tests/project/check_toml.py # vcraft.toml parser
1638
+ $ python3 tests/packaging/test_pack.py # the PyPI wheel of vcraft itself
1639
+ $ python3 tests/wheel/test_wheel.py # wheel writer, checked with zipfile and pip
1640
+ $ python3 tests/runtime/test_runtime.py # the CPython runtime
1641
+ $ python3 tests/codegen/test_codegen.py # code generator, end to end
1642
+ $ python3 tests/memory/test_leaks.py # no leaked objects, references or memory
1643
+ $ python3 tests/fuzz/test_fuzz.py # hostile arguments: no crash, the right exception
1644
+ $ python3 tests/docs/test_readme.py # every V example in this README builds
1645
+ $ scripts/run-sanitized.sh python3 tests/fuzz/test_fuzz.py # the same under ASan and UBSan (Linux)
1646
+ $ python3 tests/cli/test_cli.py # the CLI against real projects and venvs
1647
+ ```
1648
+
1649
+ They run against whichever `python3` is first on `PATH`. Inside this repository,
1650
+ invoke V only through `scripts/vcraft-v.sh`: a bare `v` answers a failed C compile
1651
+ by bootstrapping the whole compiler, a multi-gigabyte, multi-minute detour, and the
1652
+ wrapper turns that off and bounds the build's memory.
1653
+
1654
+ Releases are cut by pushing tags; see the release workflows in
1655
+ [`.github/workflows`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/.github/workflows).
1656
+
1657
+ ---
1658
+
1659
+ ## Design notes
1660
+
1661
+ A few decisions worth knowing about, and why they were made.
1662
+
1663
+ **The generated glue is V, not a foreign language.** Since the generated file is
1664
+ compiled by V alongside your code, it can call your functions with their real
1665
+ types. There is no tagged union, no serialisation step and no reflection at
1666
+ runtime, so the cost of a call is a C call plus the conversions you asked for.
1667
+
1668
+ **The CPython headers are included, never re-declared.** V does not emit
1669
+ prototypes for `fn C.` declarations, so any module that binds to CPython must
1670
+ `#include <Python.h>` rather than rely on V to declare them. See finding 1 in
1671
+ [`examples/probe/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/examples/probe/README.md).
1672
+
1673
+ **`METH_FASTCALL` is the default calling convention.** It is the cheapest way
1674
+ CPython can pass positional arguments, and it lets the generated wrapper read the
1675
+ arguments as a borrowed pointer array. Functions with parameters are registered
1676
+ as `METH_FASTCALL | METH_KEYWORDS`: a call that passes every parameter by position uses
1677
+ the array as it is, and only a call with keywords or left-out parameters is bound into
1678
+ per-parameter slots first.
1679
+
1680
+ **Annotations come from the source text.** V's parse tree does not keep
1681
+ declaration attributes; they live in the type checker. V's own `v.astquery`
1682
+ module documents the same limitation and advises reading the source. `vcraft`
1683
+ therefore parses declarations with `v.astquery` and then reads the `@[...]` block
1684
+ immediately above each one. It works on files that do not compile, which keeps
1685
+ error messages about broken annotations useful.
1686
+
1687
+ **C struct mirrors use `voidptr` and `mut:`.** V's ownership rules would demand
1688
+ initialisers for reference-typed fields, and a zero-initialised literal has to be
1689
+ able to produce C's all-zero sentinels such as the `PyMethodDef` terminator.
1690
+ Describing pointers as `voidptr` inside a `mut:` section sidesteps both.
1691
+
1692
+ **CPython data symbols get C accessors.** `PyExc_TypeError` and friends are
1693
+ data, not functions, and V cannot name a C global. The runtime ships a small C
1694
+ file of accessors with a header, pulled in with `#flag @VMODROOT/c/...`.
1695
+
1696
+ **The V garbage collector runs inside CPython.** V uses Boehm–Demers–Weiser by
1697
+ default. It is statically linked into the extension, it does not replace the C
1698
+ allocator, so it never interferes with CPython's own memory management, and V
1699
+ 0.5.2 already calls `GC_allow_register_threads()` during initialisation, which is
1700
+ precisely the case of a host-created thread entering V code.
1701
+
1702
+ **Extension modules are process-scoped.** `PyInit_` is the single-phase
1703
+ initialisation API, so a module loaded in one interpreter is shared by all of
1704
+ them. Subinterpreter support would need the multi-phase API; the generated
1705
+ glue is structured so that switch is a contained change.
1706
+
1707
+ **The runtime API is stateless.** `vcraft/args.v` is a set of plain functions and
1708
+ the generated glue keeps the call state in its own locals, passing argument indices
1709
+ explicitly. The obvious design, a reader object with a `consumed` cursor, cannot be
1710
+ compiled at all: see constraint 5 above.
1711
+
1712
+ **Nothing is installed system-wide.** `vcraft` builds into the project's `build/`
1713
+ and installs into the active virtualenv. The V runtime module ships with `vcraft`
1714
+ and is passed to the compiler with `-path`, so `VMODULES` is never touched.
1715
+
1716
+ **A module name ending in `_test` is rejected.** V would treat the file as a test
1717
+ file and silently drop its module export. `vcraft new` says so instead of producing
1718
+ an empty shared object.
1719
+
1720
+
1721
+ ### Compiler constraints
1722
+
1723
+ Building the first extension uncovered seven V compiler behaviours that produce a
1724
+ wrong answer rather than an error. They shaped the runtime and are written up in full
1725
+ in [`vlib/vcraft/README.md`](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/vlib/vcraft/README.md):
1726
+
1727
+ 1. The V C backend emits no prototypes for `fn C.` declarations, so a module that
1728
+ binds to CPython **must** `#include <Python.h>`. Without it gcc applies the
1729
+ implicit `int` return rule, truncates the returned `PyObject *` to 32 bits,
1730
+ and the interpreter segfaults on a module that loaded cleanly.
1731
+ 2. A sibling `.c.v` file only exports its declarations to the module named by its
1732
+ own `module` line.
1733
+ 3. CPython's builtin exception types are data symbols, so each one needs a small C
1734
+ accessor, with a header.
1735
+ 4. A generic function called across modules gets no forward declaration and does
1736
+ not compile, so the panic guard is inlined per trampoline instead of shared.
1737
+ 5. A `mut` receiver method on a struct from another module generates C that
1738
+ passes the struct by value where a pointer is expected. That rules out a reader
1739
+ object with a cursor, and is why the argument helpers take an explicit index.
1740
+ 6. A file matching `*_test.v` is compiled as a V test file and its module export is
1741
+ silently dropped.
1742
+ 7. The object header is 16 bytes with the GIL and 32 without it, so every struct
1743
+ mirrored from a header has two shapes. The wrong one imports on a GIL
1744
+ interpreter and segfaults on a free-threaded one with nothing pointing at the
1745
+ mirror.
1746
+
1747
+ ---
1748
+
1749
+ ## License
1750
+
1751
+ MIT. See [LICENSE](https://github.com/rroblf01/vcraft/blob/vcraft/v1.4.0/LICENSE).
1752
+
1753
+ Copyright (c) 2026 Ricardo Robles Fernández