Skip to content

Commit da5a5e4

Browse files
committed
[FEAT][STUBGEN] Add Rust code generation backend
Signed-off-by: yuchuan <yuchuan.7streams@gmail.com>
1 parent fcd5ab4 commit da5a5e4

29 files changed

Lines changed: 2926 additions & 93 deletions

File tree

cmake/Utils/Library.cmake

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ endfunction ()
172172
# target_name
173173
# [LINK_SHARED ON|OFF] [LINK_HEADER ON|OFF] [DEBUG_SYMBOL ON|OFF] [MSVC_FLAGS ON|OFF]
174174
# [STUB_INIT ON|OFF] [STUB_DIR <dir>] [STUB_PKG <pkg>] [STUB_PREFIX <prefix>]
175+
# [STUB_TARGET python|rust]
175176
# )
176177
# Configure a target to integrate with TVM-FFI CMake utilities:
177178
# - Link against tvm_ffi::header and/or tvm_ffi::shared
@@ -194,6 +195,11 @@ endfunction ()
194195
# STUB_INIT: Whether to allow generating new directives. Default: OFF (ON/OFF-style)
195196
# STUB_PKG: Package name passed to stub generator (requires STUB_DIR and STUB_INIT=ON; default: ${SKBUILD_PROJECT_NAME} if set, otherwise target name)
196197
# STUB_PREFIX: Module prefix passed to stub generator (requires STUB_DIR and STUB_INIT=ON; default: "<STUB_PKG>.")
198+
# STUB_TARGET: Code generator backend: "python" (default) or "rust". Passed to the stub
199+
# generator as --target. With "rust", object bindings are emitted into a Rust
200+
# module tree under STUB_DIR (global functions are not generated for Rust).
201+
# To generate both Python and Rust stubs for one target, call this function
202+
# twice with different STUB_DIR/STUB_TARGET values.
197203
# ~~~
198204
function (tvm_ffi_configure_target target)
199205
if (NOT target)
@@ -219,6 +225,7 @@ function (tvm_ffi_configure_target target)
219225
STUB_DIR
220226
STUB_PKG
221227
STUB_PREFIX
228+
STUB_TARGET
222229
)
223230
set(tvm_ffi_arg_multiValueArgs)
224231

@@ -236,8 +243,17 @@ function (tvm_ffi_configure_target target)
236243
if (NOT DEFINED tvm_ffi_arg__STUB_INIT)
237244
set(tvm_ffi_arg__STUB_INIT OFF)
238245
endif ()
246+
if (NOT DEFINED tvm_ffi_arg__STUB_TARGET)
247+
set(tvm_ffi_arg__STUB_TARGET "python")
248+
endif ()
239249

240250
# Validation
251+
if (NOT tvm_ffi_arg__STUB_TARGET MATCHES "^(python|rust)$")
252+
message(
253+
FATAL_ERROR
254+
"tvm_ffi_configure_target(${target}): STUB_TARGET must be 'python' or 'rust', got '${tvm_ffi_arg__STUB_TARGET}'."
255+
)
256+
endif ()
241257
if ((NOT DEFINED tvm_ffi_arg__STUB_DIR) OR (NOT tvm_ffi_arg__STUB_DIR))
242258
if (DEFINED tvm_ffi_arg__STUB_PKG OR DEFINED tvm_ffi_arg__STUB_PREFIX)
243259
message(
@@ -346,7 +362,9 @@ function (tvm_ffi_configure_target target)
346362
COMPONENTS Interpreter
347363
REQUIRED
348364
)
349-
set(tvm_ffi_stub_cli_args "${tvm_ffi_arg__STUB_DIR_ABS}" --dlls $<TARGET_FILE:${target}>)
365+
set(tvm_ffi_stub_cli_args "${tvm_ffi_arg__STUB_DIR_ABS}" --dlls $<TARGET_FILE:${target}>
366+
--target "${tvm_ffi_arg__STUB_TARGET}"
367+
)
350368
if (tvm_ffi_arg__STUB_INIT)
351369
list(
352370
APPEND

docs/guides/rust_lang_guide.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,22 @@ fn may_fail(value: i32) -> Result<()> {
180180
}
181181
```
182182

183+
### Generating Bindings with stubgen
184+
185+
For registered C++ classes, `tvm-ffi-stubgen --target rust` generates typed Rust
186+
bindings whose memory layout mirrors C++ exactly, so you can construct and call
187+
objects without hand-written glue:
188+
189+
```rust
190+
// generated for a C++ class `my_ext.IntPair` (fields `a`/`b`, defaulted `scale`, method `sum`)
191+
let mut pair = IntPair::ffi_new().a(1).b(2).build()?; // builder; `scale` defaults to 1
192+
println!("sum = {}", pair.sum()?); // call a C++ method
193+
pair.a = 10; // write a field through DerefMut
194+
```
195+
196+
See {ref}`sec-stubgen-rust` for the full reference and the runnable
197+
[`examples/rust_stubgen`](https://github.com/apache/tvm-ffi/tree/main/examples/rust_stubgen).
198+
183199
## Examples
184200

185201
The repository includes a complete example in `rust/tvm-ffi/examples/load_library.rs`.
@@ -213,5 +229,6 @@ For detailed API documentation, see the [Rust API Reference](../reference/rust/i
213229
## Related Resources
214230

215231
- [Quick Start Guide](../get_started/quickstart.rst) - General TVM FFI introduction
232+
- [Stub Generation](../packaging/stubgen.rst) - Full `tvm-ffi-stubgen` reference (Python and Rust targets)
216233
- [C++ Guide](./cpp_lang_guide.md) - C++ API usage
217234
- [Python Guide](./python_lang_guide.md) - Python API usage

docs/packaging/stubgen.rst

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
Stub Generation
2121
===============
2222

23-
TVM-FFI provides ``tvm-ffi-stubgen``, a tool that generates Python type stubs from C++
24-
reflection metadata. It turns registered global functions and classes into proper Python
25-
type hints, enabling IDE auto-completion and static type checking.
23+
TVM-FFI provides ``tvm-ffi-stubgen``, a tool that generates typed language bindings from C++
24+
reflection metadata. It turns registered global functions and classes into native type hints,
25+
enabling IDE auto-completion and static type checking.
26+
27+
The sections below describe the default Python target. The Rust target is covered in
28+
:ref:`sec-stubgen-rust`.
2629

2730
.. admonition:: Prerequisite
2831
:class: hint
@@ -46,6 +49,7 @@ This runs stub generation automatically after each build.
4649
[STUB_INIT ON|OFF]
4750
[STUB_PKG <pkg>]
4851
[STUB_PREFIX <prefix>]
52+
[STUB_TARGET python|rust]
4953
)
5054
5155
From the example's
@@ -239,6 +243,9 @@ All three are required together. When omitted, the tool operates in directive-on
239243

240244
**Optional arguments:**
241245

246+
``--target``
247+
Code generator backend: ``python`` (default) or ``rust``. See :ref:`sec-stubgen-rust`.
248+
242249
``--verbose``
243250
Print a unified diff of changes to each file.
244251

@@ -253,6 +260,153 @@ All three are required together. When omitted, the tool operates in directive-on
253260

254261
For a complete list of options, run ``tvm-ffi-stubgen --help``.
255262

263+
.. _sec-stubgen-rust:
264+
265+
Rust Code Stubgen (Experimental)
266+
--------------------------------
267+
268+
TVM-FFI provides an efficient, easy-to-use mechanism for exposing C++ classes to Rust.
269+
Objects share the same memory representation, so Rust code can directly access objects
270+
created in C++. However, users have to hand-write the Rust definition of each registered
271+
object to avoid memory layout and alignment mismatches between the two sides. To eliminate
272+
this manual work, ``tvm-ffi-stubgen`` supports generating Rust code directly.
273+
274+
To generate Rust stubs, pass ``STUB_TARGET rust`` to ``tvm_ffi_configure_target``
275+
(see :ref:`sec-stubgen-cmake`) or ``--target rust`` on the command line
276+
(see :ref:`sec-stubgen-cli`).
277+
278+
Key Features
279+
~~~~~~~~~~~~
280+
281+
- Generate Rust code for registered C++ classes automatically (via CLI or CMake).
282+
- Mirror the C++ memory layout exactly in Rust.
283+
- Provide Rust-native builder-style constructors for registered classes, with
284+
reflected default values prefilled and overridable through setters.
285+
- Expose methods of registered classes through cross-language calls.
286+
287+
Generation Output
288+
~~~~~~~~~~~~~~~~~
289+
290+
This section uses `examples/rust_stubgen <https://github.com/apache/tvm-ffi/tree/main/examples/rust_stubgen>`_
291+
as the running example. The library registers a single type, ``rust_stubgen.IntPair``,
292+
with two required fields ``a`` / ``b``, a defaulted field ``scale``, and a method ``sum``:
293+
294+
.. literalinclude:: ../../examples/rust_stubgen/src/int_pair.cc
295+
:language: cpp
296+
:start-after: [object.begin]
297+
:end-before: [object.end]
298+
299+
For this type the tool generates the following Rust code (bodies abridged):
300+
301+
.. code-block:: rust
302+
303+
#[repr(C)]
304+
#[derive(tvm_ffi::derive::Object)]
305+
#[type_key = "rust_stubgen.IntPair"]
306+
pub struct IntPairObj {
307+
base: Object, // the parent type, embedded as the first field
308+
pub a: i64,
309+
pub b: i64,
310+
pub scale: i64,
311+
}
312+
313+
#[repr(C)]
314+
#[derive(tvm_ffi::derive::ObjectRef, Clone)]
315+
pub struct IntPair {
316+
data: ObjectArc<IntPairObj>,
317+
}
318+
319+
impl IntPair {
320+
pub fn ffi_new() -> IntPairBuilder { /* ... */ }
321+
pub fn sum(&mut self) -> Result<i64> { /* ... */ }
322+
}
323+
324+
pub struct IntPairBuilder { /* base + every field */ }
325+
326+
impl IntPairBuilder {
327+
pub fn a(mut self, a: i64) -> Self { /* ... */ }
328+
pub fn b(mut self, b: i64) -> Self { /* ... */ }
329+
pub fn scale(mut self, scale: i64) -> Self { /* ... */ }
330+
pub fn build(self) -> Result<IntPair> { /* ... */ }
331+
pub fn build_obj(self) -> Result<IntPairObj> { /* ... */ }
332+
}
333+
334+
``IntPairObj`` mirrors the C++ memory layout exactly, so Rust code can directly
335+
access objects created in C++ and vice versa. ``IntPair`` is the reference type
336+
that owns an allocation of it; fields are read through ``Deref`` (and written
337+
through ``DerefMut``, since the class declares ``_type_mutable = true``), and
338+
``sum`` calls into the C++ implementation through the FFI.
339+
340+
Builder-style Construction
341+
~~~~~~~~~~~~~~~~~~~~~~~~~~
342+
343+
Construction is fully Rust-native -- no FFI call is involved -- and uniform: a
344+
nullary ``ffi_new()`` opens the builder, every field is set through its
345+
like-named consuming setter, and ``build()`` finishes the chain:
346+
347+
.. code-block:: rust
348+
349+
let pair = IntPair::ffi_new().a(1).b(2).build()?; // scale = 1 (default)
350+
let scaled = IntPair::ffi_new().a(1).b(2).scale(10).build()?; // override the default
351+
let err = IntPair::ffi_new().a(1).build(); // Err: field `b` is not set
352+
353+
``ffi_new() -> IntPairBuilder``
354+
Opens the builder. A field with a ``refl::default_value`` (here ``scale``)
355+
starts prefilled with its default, rendered as a Rust literal at
356+
stub-generation time; every other field starts unset.
357+
358+
``a(..)`` / ``b(..)`` / ``scale(..)``
359+
One consuming setter per field. Setting a defaulted field overrides its
360+
default.
361+
362+
``build() -> Result<IntPair>``
363+
Validates and allocates: returns an error if a field without a default is
364+
still unset (the ``err`` case above), otherwise wraps the assembled value
365+
in ``ObjectArc`` and returns the reference type. This is the endpoint to
366+
use in ordinary code.
367+
368+
``build_obj() -> Result<IntPairObj>``
369+
Performs the same validation and assembly as ``build()`` -- ``build()`` in
370+
fact delegates to it -- but stops at the bare, unallocated struct value.
371+
It exists for inheritance: a C++ class deriving from ``IntPair`` embeds
372+
``IntPairObj`` as its first field, and the derived type's generated builder
373+
gains a ``base(..)`` setter that takes exactly this value:
374+
375+
.. code-block:: rust
376+
377+
// for a hypothetical `Derived` extending IntPair with a field `c`
378+
let d = Derived::ffi_new()
379+
.base(IntPair::ffi_new().a(1).b(2).build_obj()?)
380+
.c(3)
381+
.build()?;
382+
383+
When ``base`` is left unset, the derived ``build()`` falls back to
384+
default-constructing the parent through its all-default builder. This
385+
succeeds silently when every parent field has a default; for ``IntPair``
386+
it would fail with an error naming ``base``, since ``a`` and ``b`` carry
387+
no default.
388+
389+
The builder deliberately bypasses any C++ constructor logic (it never runs
390+
``IntPairObj``'s C++ constructor); users who need the faithful C++ semantics
391+
can hand-write a ``new`` constructor (outside the generated markers) on top of
392+
the builder.
393+
394+
Limitations
395+
~~~~~~~~~~~
396+
397+
A type that mentions an origin the Rust crate cannot represent -- in any
398+
position: field, method argument, return type, or nested inside another
399+
container -- is explicitly unsupported: the whole binding is skipped with a
400+
warning. This covers ``Map`` / ``Dict`` / ``List`` / ``Union`` (no Rust
401+
counterpart) as well as ``Optional`` / ``tuple`` (std ``Option<T>`` and Rust
402+
tuples do not match the C++ ``ffi::Optional`` / ``ffi::Tuple`` memory layout).
403+
404+
A constructor alone cannot be generated when a default comes from a
405+
``refl::default_factory`` or when a default value has no Rust literal
406+
rendering: such types are emitted without ``ffi_new`` (a warning explains why),
407+
and construction stays on the C++ side -- e.g. through a ``def_static``
408+
factory.
409+
256410
.. _sec-stubgen-advanced:
257411

258412
Advanced Topics
@@ -391,7 +545,8 @@ When you run the tool, it:
391545
# tvm-ffi-stubgen(import-object): ffi.Object;False;_ffi_Object
392546
393547
This imports ``ffi.Object`` as ``_ffi_Object`` for use in generated code. The second
394-
field (``False``) indicates the import is not TYPE_CHECKING-only.
548+
field (``False``) indicates the import is not TYPE_CHECKING-only. The Rust target
549+
records the name as a plain ``use`` and ignores the other two fields.
395550

396551
``skip-file`` - Skip File
397552
Prevents the tool from modifying the file. Place anywhere in the file.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
cmake_minimum_required(VERSION 3.18)
18+
project(rust_stubgen)
19+
20+
find_package(
21+
Python
22+
COMPONENTS Interpreter
23+
REQUIRED
24+
)
25+
execute_process(
26+
COMMAND "${Python_EXECUTABLE}" -m tvm_ffi.config --cmakedir
27+
OUTPUT_STRIP_TRAILING_WHITESPACE
28+
OUTPUT_VARIABLE tvm_ffi_ROOT COMMAND_ERROR_IS_FATAL ANY
29+
)
30+
find_package(tvm_ffi CONFIG REQUIRED)
31+
# [example.cmake.begin]
32+
add_library(rust_stubgen SHARED src/int_pair.cc)
33+
tvm_ffi_configure_target(
34+
rust_stubgen
35+
STUB_DIR
36+
"./rust/src/generated"
37+
STUB_TARGET
38+
rust
39+
STUB_INIT
40+
ON
41+
)
42+
# [example.cmake.end]

examples/rust_stubgen/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!--- Licensed to the Apache Software Foundation (ASF) under one -->
2+
<!--- or more contributor license agreements. See the NOTICE file -->
3+
<!--- distributed with this work for additional information -->
4+
<!--- regarding copyright ownership. The ASF licenses this file -->
5+
<!--- to you under the Apache License, Version 2.0 (the -->
6+
<!--- "License"); you may not use this file except in compliance -->
7+
<!--- with the License. You may obtain a copy of the License at -->
8+
9+
<!--- http://www.apache.org/licenses/LICENSE-2.0 -->
10+
11+
<!--- Unless required by applicable law or agreed to in writing, -->
12+
<!--- software distributed under the License is distributed on an -->
13+
<!--- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -->
14+
<!--- KIND, either express or implied. See the License for the -->
15+
<!--- specific language governing permissions and limitations -->
16+
<!--- under the License. -->
17+
18+
# TVM FFI Rust Stubgen Example
19+
20+
This is an example project that registers a C++ object (`IntPair`) and
21+
generates typed Rust bindings for it with `tvm-ffi-stubgen --target rust`,
22+
as documented in [docs/packaging/stubgen.rst](../../docs/packaging/stubgen.rst).
23+
24+
## Build the library and generate the bindings
25+
26+
Install tvm-ffi and activate the virtualenv first (from the repo root):
27+
28+
```bash
29+
uv pip install -e .
30+
source .venv/bin/activate
31+
```
32+
33+
Then build the C++ shared library:
34+
35+
```bash
36+
cd examples/rust_stubgen
37+
cmake -B build
38+
cmake --build build
39+
```
40+
41+
Stub generation runs as a post-build step
42+
(`tvm_ffi_configure_target(... STUB_TARGET rust STUB_INIT ON)` in
43+
`CMakeLists.txt`) and refreshes `rust/src/generated/`. The equivalent CLI
44+
invocation is:
45+
46+
```bash
47+
tvm-ffi-stubgen rust/src/generated --target rust --dlls build/librust_stubgen.so \
48+
--init-lib rust_stubgen --init-pypkg rust_stubgen --init-prefix "rust_stubgen."
49+
```
50+
51+
## Run the example
52+
53+
After building the C++ library, run the Rust demo:
54+
55+
```bash
56+
cd rust
57+
cargo run
58+
```
59+
60+
This runs four flows: constructing an `IntPair` via the generated builder
61+
(`ffi_new().a(1).b(2).build()`; the defaulted `scale` field may be omitted),
62+
calling the `sum` method, overriding the default through the `.scale(..)`
63+
setter, and writing a field through `DerefMut`.

0 commit comments

Comments
 (0)