Skip to content

Commit 401b50d

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

28 files changed

Lines changed: 3010 additions & 84 deletions

File tree

cmake/Utils/Library.cmake

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ endfunction ()
149149
# target_name
150150
# [LINK_SHARED ON|OFF] [LINK_HEADER ON|OFF] [DEBUG_SYMBOL ON|OFF] [MSVC_FLAGS ON|OFF]
151151
# [STUB_INIT ON|OFF] [STUB_DIR <dir>] [STUB_PKG <pkg>] [STUB_PREFIX <prefix>]
152+
# [STUB_TARGET python|rust]
152153
# )
153154
# Configure a target to integrate with TVM-FFI CMake utilities:
154155
# - Link against tvm_ffi::header and/or tvm_ffi::shared
@@ -171,6 +172,11 @@ endfunction ()
171172
# STUB_INIT: Whether to allow generating new directives. Default: OFF (ON/OFF-style)
172173
# STUB_PKG: Package name passed to stub generator (requires STUB_DIR and STUB_INIT=ON; default: ${SKBUILD_PROJECT_NAME} if set, otherwise target name)
173174
# STUB_PREFIX: Module prefix passed to stub generator (requires STUB_DIR and STUB_INIT=ON; default: "<STUB_PKG>.")
175+
# STUB_TARGET: Code generator backend: "python" (default) or "rust". Passed to the stub
176+
# generator as --target. With "rust", object bindings are emitted into a Rust
177+
# module tree under STUB_DIR (global functions are not generated for Rust).
178+
# To generate both Python and Rust stubs for one target, call this function
179+
# twice with different STUB_DIR/STUB_TARGET values.
174180
# ~~~
175181
function (tvm_ffi_configure_target target)
176182
if (NOT target)
@@ -196,6 +202,7 @@ function (tvm_ffi_configure_target target)
196202
STUB_DIR
197203
STUB_PKG
198204
STUB_PREFIX
205+
STUB_TARGET
199206
)
200207
set(tvm_ffi_arg_multiValueArgs)
201208

@@ -213,8 +220,17 @@ function (tvm_ffi_configure_target target)
213220
if (NOT DEFINED tvm_ffi_arg__STUB_INIT)
214221
set(tvm_ffi_arg__STUB_INIT OFF)
215222
endif ()
223+
if (NOT DEFINED tvm_ffi_arg__STUB_TARGET)
224+
set(tvm_ffi_arg__STUB_TARGET "python")
225+
endif ()
216226

217227
# Validation
228+
if (NOT tvm_ffi_arg__STUB_TARGET MATCHES "^(python|rust)$")
229+
message(
230+
FATAL_ERROR
231+
"tvm_ffi_configure_target(${target}): STUB_TARGET must be 'python' or 'rust', got '${tvm_ffi_arg__STUB_TARGET}'."
232+
)
233+
endif ()
218234
if ((NOT DEFINED tvm_ffi_arg__STUB_DIR) OR (NOT tvm_ffi_arg__STUB_DIR))
219235
if (DEFINED tvm_ffi_arg__STUB_PKG OR DEFINED tvm_ffi_arg__STUB_PREFIX)
220236
message(
@@ -323,7 +339,9 @@ function (tvm_ffi_configure_target target)
323339
COMPONENTS Interpreter
324340
REQUIRED
325341
)
326-
set(tvm_ffi_stub_cli_args "${tvm_ffi_arg__STUB_DIR_ABS}" --dlls $<TARGET_FILE:${target}>)
342+
set(tvm_ffi_stub_cli_args "${tvm_ffi_arg__STUB_DIR_ABS}" --dlls $<TARGET_FILE:${target}>
343+
--target "${tvm_ffi_arg__STUB_TARGET}"
344+
)
327345
if (tvm_ffi_arg__STUB_INIT)
328346
list(
329347
APPEND

docs/packaging/stubgen.rst

Lines changed: 126 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,120 @@ 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+
For the type ``IntPair`` (fields ``a`` / ``b``, plus ``scale`` registered with
291+
``refl::default_value(1)``), the generated Rust code looks like this:
292+
293+
.. code-block:: rust
294+
295+
#[repr(C)]
296+
#[derive(tvm_ffi::derive::Object)]
297+
#[type_key = "my_ffi_extension.IntPair"]
298+
pub struct IntPairObj {
299+
base: Object, // the parent type (or `tvm_ffi::Object` if no parent), embedded as the first field
300+
pub a: i64, // fields are public: read (and write, if mutable) via `Deref`/`DerefMut`
301+
pub b: i64,
302+
pub scale: i64,
303+
}
304+
305+
#[repr(C)]
306+
#[derive(tvm_ffi::derive::ObjectRef, Clone)]
307+
pub struct IntPair {
308+
data: ObjectArc<IntPairObj>,
309+
}
310+
311+
impl IntPair {
312+
/// Native (FFI-free) construction: opens the builder. `scale` starts
313+
/// prefilled with its reflected default (`1`); `a` / `b` start unset.
314+
pub fn ffi_new() -> IntPairBuilder { /* ... */ }
315+
}
316+
317+
pub struct IntPairBuilder { /* base + every own field */ }
318+
319+
impl IntPairBuilder {
320+
/// One consuming setter per field -- one uniform API.
321+
pub fn a(mut self, a: i64) -> Self { /* ... */ }
322+
pub fn b(mut self, b: i64) -> Self { /* ... */ }
323+
pub fn scale(mut self, scale: i64) -> Self { /* ... */ }
324+
/// Allocates the object (`ObjectArc::new`); errors if a field without
325+
/// a default is still unset.
326+
pub fn build(self) -> Result<IntPair> { /* ... */ }
327+
/// The bare struct value -- what a derived type's `base(...)` setter takes.
328+
pub fn build_obj(self) -> Result<IntPairObj> { /* ... */ }
329+
}
330+
331+
The object has the same memory layout as its C++ counterpart. Users can create new objects
332+
on the Rust side and call the methods defined in C++:
333+
334+
.. code-block:: rust
335+
336+
let pair = IntPair::ffi_new().a(1).b(2).build()?; // scale = 1 (default)
337+
let scaled = IntPair::ffi_new().a(1).b(2).scale(10).build()?; // override via setter
338+
let err = IntPair::ffi_new().a(1).build(); // Err: field `b` is not set
339+
340+
Construction is fully Rust-native and the API is uniform: ``ffi_new()`` is
341+
nullary for root and derived types alike, and every input -- own fields and a
342+
derived type's ``base`` -- is set through its like-named consuming setter.
343+
Fields with a ``refl::default_value`` are rendered as Rust literals at
344+
stub-generation time and prefilled in the builder; fields without a default
345+
start unset, and ``build()`` returns an error if any is still missing. The
346+
builder deliberately bypasses any C++ constructor logic; users who need the
347+
faithful C++ semantics can hand-write a ``new`` constructor (outside the
348+
generated markers) on top of the builder.
349+
350+
A derived type's builder feeds its embedded parent through ``base(...)``,
351+
which takes the bare parent value produced by the parent builder's
352+
``build_obj()`` (emitted on every builder):
353+
354+
.. code-block:: rust
355+
356+
let p3 = Point3D::ffi_new()
357+
.base(Point::ffi_new().x(1).y(2).build_obj()?)
358+
.z(3)
359+
.build()?;
360+
361+
When ``base`` is left unset, ``build()`` default-constructs the parent through
362+
its all-default builder -- silently when every parent field has a default, and
363+
with a build-time error naming ``base`` when one does not.
364+
365+
A type that mentions an origin the Rust crate cannot represent -- in any
366+
position: field, method argument, return type, or nested inside another
367+
container -- is explicitly unsupported: the whole binding is skipped with a
368+
warning. This covers ``Map`` / ``Dict`` / ``List`` / ``Union`` (no Rust
369+
counterpart) as well as ``Optional`` / ``tuple`` (std ``Option<T>`` and Rust
370+
tuples do not match the C++ ``ffi::Optional`` / ``ffi::Tuple`` memory layout).
371+
A constructor alone cannot be generated when a default comes from a
372+
``refl::default_factory`` or when a default value has no Rust literal
373+
rendering: such types are emitted without ``ffi_new`` (a warning explains why),
374+
and construction stays on the C++ side -- e.g. through a ``def_static``
375+
factory.
376+
256377
.. _sec-stubgen-advanced:
257378

258379
Advanced Topics
@@ -391,7 +512,8 @@ When you run the tool, it:
391512
# tvm-ffi-stubgen(import-object): ffi.Object;False;_ffi_Object
392513
393514
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.
515+
field (``False``) indicates the import is not TYPE_CHECKING-only. The Rust target
516+
records the name as a plain ``use`` and ignores the other two fields.
395517

396518
``skip-file`` - Skip File
397519
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`.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
[package]
19+
name = "rust_stubgen_example"
20+
version = "0.1.0"
21+
edition = "2021"
22+
license = "Apache-2.0"
23+
publish = false
24+
25+
[dependencies]
26+
tvm-ffi = { path = "../../../rust/tvm-ffi" }

0 commit comments

Comments
 (0)