This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 426ec96  [DOCS] Update packaging and binding guide (#164)
426ec96 is described below

commit 426ec96c1b9234d1f63ed430d315e9c5ea5628c1
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Oct 17 14:35:09 2025 -0400

    [DOCS] Update packaging and binding guide (#164)
    
    This PR provides an updates to the python packaging and binding guide.
---
 docs/guides/{packaging.md => python_packaging.md}  | 201 +++++++++++++++++++--
 docs/index.rst                                     |   2 +-
 .../packaging/python/my_ffi_extension/__init__.py  |  15 +-
 examples/packaging/run_example.py                  |  11 +-
 examples/packaging/src/extension.cc                |  42 +++++
 5 files changed, 255 insertions(+), 16 deletions(-)

diff --git a/docs/guides/packaging.md b/docs/guides/python_packaging.md
similarity index 52%
rename from docs/guides/packaging.md
rename to docs/guides/python_packaging.md
index 4979d02..0c191e9 100644
--- a/docs/guides/packaging.md
+++ b/docs/guides/python_packaging.md
@@ -14,21 +14,196 @@
 <!--- KIND, either express or implied.  See the License for the -->
 <!--- specific language governing permissions and limitations -->
 <!--- under the License. -->
-# Packaging
+# Python Binding and Packaging
 
-This guide explains how to package a tvm-ffi-based library into a Python 
ABI-agnostic wheel.
-It demonstrates both source-level builds (for cross-compilation) and builds 
based on pre-shipped shared libraries.
+This guide explains how to leverage tvm-ffi to expose C++ functions into 
Python and package them into a wheel.
 At a high level, packaging with tvm-ffi offers several benefits:
 
-- **ABI-agnostic wheels**: Works across different Python versions with minimal 
dependency.
-- **Universally deployable**: Build once with tvm-ffi and ship to different 
environments, including Python and non-Python environments.
+- **Ship one wheel** that can be used across Python versions, including 
free-threaded Python.
+- **Multi-language access** to functions from Python, C++, Rust and other 
languages that connect to the ABI.
+- **ML Systems Interop** with ML frameworks, DSLs, and libraries while 
maintaining minimal dependency.
 
-While this guide shows how to build a wheel package, the resulting 
`my_ffi_extension.so` is agnostic
-to Python, comes with minimal dependencies, and can be used in other 
deployment scenarios.
+## Directly using Exported Library
 
-## Build and Run the Example
+If you just need to expose a simple set of functions,
+you can declare an exported symbol in C++:
 
-Let's start by building and running the example.
+```c++
+// Compiles to mylib.so
+#include <tvm/ffi/function.h>
+
+int add_one(int x) {
+  return x + 1;
+}
+
+TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, add_one)
+```
+
+You then load the exported function in your Python project via 
{py:func}`tvm_ffi.load_module`.
+
+```python
+# In your __init__.py
+import tvm_ffi
+
+_LIB = tvm_ffi.load_module("/path/to/mlib.so")
+
+def add_one(x):
+    """Expose mylib.add_one"""
+    return _LIB.add_one(x)
+```
+
+This approach is like using {py:mod}`ctypes` to load and run DLLs, except we 
have more powerful features:
+
+- We can pass in and return a richer set of data types such as 
{py:class}`tvm_ffi.Tensor` and strings.
+- {py:class}`tvm_ffi.Function` enables natural callbacks to Python lambdas or 
other languages.
+- Exceptions are propagated naturally across language boundaries.
+
+## Pybind11 and Nanobind style Usage
+
+For advanced use cases where users may wish to register global functions or 
custom object types,
+we also provide a pybind11/nanobind style API to register functions and custom 
objects.
+
+```c++
+#include <tvm/ffi/error.h>
+#include <tvm/ffi/reflection/registry.h>
+
+namespace my_ffi_extension {
+
+namespace ffi = tvm::ffi;
+
+/*!
+ * \brief Example of a custom object that is exposed to the FFI library
+ */
+class IntPairObj : public ffi::Object {
+ public:
+  int64_t a;
+  int64_t b;
+
+  IntPairObj() = default;
+  IntPairObj(int64_t a, int64_t b) : a(a), b(b) {}
+
+  int64_t GetFirst() const { return this->a; }
+
+  // Required: declare type information
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ffi_extension.IntPair", IntPairObj, 
ffi::Object);
+};
+
+/*!
+ * \brief Defines an explicit reference to IntPairObj
+ *
+ * A reference wrapper serves as a reference-counted pointer to the object.
+ * You can use obj->field to access the fields of the object.
+ */
+class IntPair : public tvm::ffi::ObjectRef {
+ public:
+  // Constructor
+  explicit IntPair(int64_t a, int64_t b) {
+    data_ = tvm::ffi::make_object<IntPairObj>(a, b);
+  }
+
+  // Required: define object reference methods
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntPair, tvm::ffi::ObjectRef, 
IntPairObj);
+};
+
+void RaiseError(ffi::String msg) { TVM_FFI_THROW(RuntimeError) << msg; }
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  refl::GlobalDef()
+    .def("my_ffi_extension.raise_error", RaiseError);
+  // register object definition
+  refl::ObjectDef<IntPairObj>()
+      .def(refl::init<int64_t, int64_t>())
+       // Example static method that returns the second element of the pair
+      .def_static("static_get_second", [](IntPair pair) -> int64_t { return 
pair->b; })
+      // Example to bind an instance method
+      .def("get_first", &IntPairObj::GetFirst)
+      .def_ro("a", &IntPairObj::a)
+      .def_ro("b", &IntPairObj::b);
+}
+}  // namespace my_ffi_extension
+```
+
+Then these functions and objects can be accessed from Python as long as the 
library is loaded.
+You can use {py:func}`tvm_ffi.load_module` or simply use 
{py:class}`ctypes.CDLL`. Then you can access
+the function through {py:func}`tvm_ffi.get_global_func` or 
{py:func}`tvm_ffi.init_ffi_api`.
+We also allow direct exposure of object via {py:func}`tvm_ffi.register_object`.
+
+```python
+# __init__.py
+import tvm_ffi
+
+def raise_error(msg: str):
+    """Wrap raise error function."""
+    # Usually we reorganize these functions into a _ffi_api.py and load once
+    func = tvm_ffi.get_global_func("my_ffi_extension.raise_error")
+    func(msg)
+
+
+@tvm_ffi.register_object("my_ffi_extension.IntPair")
+class IntPair(tvm_ffi.Object):
+    """IntPair object."""
+
+    def __init__(self, a: int, b: int) -> None:
+        """Construct the object."""
+        # __ffi_init__ call into the refl::init<> registered
+        # in the static initialization block of the extension library
+        self.__ffi_init__(a, b)
+
+
+def run_example():
+    pair = IntPair(1, 2)
+    # prints 1
+    print(pair.get_first())
+    # prints 2
+    print(IntPair.static_get_second(pair))
+    # Raises a RuntimeError("error happens")
+    raise_error("error happens")
+```
+
+### Relations to Existing Solutions
+
+Most current binding systems focus on creating one-to-one bindings
+that take a source language and bind to an existing target language runtime 
and ABI.
+We deliberately take a more decoupled approach here:
+
+- Build stable, minimal ABI convention that is agnostic to the target language.
+- Create bindings to connect the source and target language to the ABI.
+
+The focus of this project is the ABI itself which we believe can help the 
overall ecosystem.
+We also anticipate there are possibilities for existing binding generators to 
also target the tvm-ffi ABI.
+
+**Design philosophy**. We have the following design philosophies focusing on 
ML systems.
+
+- FFI and cross-language interop should be first-class citizens in ML systems 
rather than an add-on.
+- Enable multi-environment support in both source and target languages.
+- The same ABI should be minimal and targetable by DSL compilers.
+
+Of course, there is always a tradeoff. It is by design impossible to support 
arbitrary advanced language features
+in the target language, as different programming languages have their own 
design considerations.
+We do believe it is possible to build a universal, effective, and minimal ABI 
for machine learning
+system use cases. Based on the above design philosophies, we focus our 
cross-language
+interaction interface through the FFI ABI for machine learning systems.
+
+So if you are building projects related to machine learning compilers, 
runtimes,
+libraries, frameworks, DSLs, or generally scientific computing, we encourage 
you
+to try it out. The extension mechanism can likely support features in other 
domains as well
+and we welcome you to try it out as well.
+
+### Mix with Existing Solutions
+
+Because the global registry mechanism only relies on the code being linked,
+you can also partially use tvm-ffi-based registration together with 
pybind11/nanobind in your project.
+Just add the related code, link to `libtvm_ffi` and make sure you `import 
tvm_ffi` before importing
+your module to ensure related symbols are available.
+This approach may help to quickly leverage some of the cross-language features 
we have.
+It also provides more powerful interaction with the host Python language, but 
of course the tradeoff
+is that the final library will now also depend on the Python ABI.
+
+## Example Project Walk Through
+
+To get hands-on experience with the packaging flow,
+you can try out an example project in our folder.
 First, obtain a copy of the tvm-ffi source code.
 
 ```bash
@@ -37,7 +212,7 @@ cd tvm-ffi
 ```
 
 The examples are now in the examples folder. You can quickly build
-and install the example using the following command.
+and install the example using the following commands.
 
 ```bash
 cd examples/packaging
@@ -88,7 +263,7 @@ wheel.py-api = "py3"
 We use scikit-build-core for building the wheel. Make sure you add tvm-ffi as 
a build-system requirement.
 Importantly, we should set `wheel.py-api` to `py3` to indicate it is 
ABI-generic.
 
-## Setup CMakeLists.txt
+### Setup CMakeLists.txt
 
 The CMakeLists.txt handles the build and linking of the project.
 There are two ways you can build with tvm-ffi:
@@ -136,7 +311,7 @@ In Python or other cases when we dynamically load 
libtvm_ffi shipped with the de
 you do not need to ship libtvm_ffi.so in your package even if you build 
tvm-ffi from source.
 The built objects are only used to supply the linking information.
 
-## Exposing C++ Functions
+### Exposing C++ Functions
 
 The C++ implementation is defined in `src/extension.cc`.
 There are two ways one can expose a function in C++ to the FFI library.
@@ -175,7 +350,7 @@ and is expected to stay throughout the lifetime of the 
program.
 We recommend using `TVM_FFI_DLL_EXPORT_TYPED_FUNC` for functions that are 
supposed to be dynamically
 loaded (such as JIT scenarios) so they won't be exposed to the global function 
table.
 
-## Library Loading in Python
+### Library Loading in Python
 
 The base module handles loading the compiled extension:
 
diff --git a/docs/index.rst b/docs/index.rst
index b3a7519..ecea8e6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -45,7 +45,7 @@ Table of Contents
    :maxdepth: 1
    :caption: Guides
 
-   guides/packaging.md
+   guides/python_packaging.md
    guides/cpp_guide.md
    guides/python_guide.md
    guides/stable_c_abi.md
diff --git a/examples/packaging/python/my_ffi_extension/__init__.py 
b/examples/packaging/python/my_ffi_extension/__init__.py
index ae4abfd..292a03b 100644
--- a/examples/packaging/python/my_ffi_extension/__init__.py
+++ b/examples/packaging/python/my_ffi_extension/__init__.py
@@ -17,12 +17,25 @@
 # isort: skip_file
 """Public Python API for the example tvm-ffi extension package."""
 
-from typing import Any
+from typing import Any, TYPE_CHECKING
+
+import tvm_ffi
 
 from .base import _LIB
 from . import _ffi_api
 
 
+@tvm_ffi.register_object("my_ffi_extension.IntPair")
+class IntPair(tvm_ffi.Object):
+    """IntPair object."""
+
+    def __init__(self, a: int, b: int) -> None:
+        """Construct the object."""
+        # __ffi_init__ call into the refl::init<> registered
+        # in the static initialization block of the extension library
+        self.__ffi_init__(a, b)
+
+
 def add_one(x: Any, y: Any) -> None:
     """Add one to the input tensor.
 
diff --git a/examples/packaging/run_example.py 
b/examples/packaging/run_example.py
index 6b5120f..f2c79f4 100644
--- a/examples/packaging/run_example.py
+++ b/examples/packaging/run_example.py
@@ -35,11 +35,20 @@ def run_raise_error() -> None:
     my_ffi_extension.raise_error("This is an error")
 
 
+def run_int_pair() -> None:
+    """Invoke IntPair from the extension to demonstrate object handling."""
+    pair = my_ffi_extension.IntPair(1, 2)
+    print(f"first={pair.get_first()}")
+    print(f"second={my_ffi_extension.IntPair.static_get_second(pair)}")
+
+
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         if sys.argv[1] == "add_one":
             run_add_one()
+        elif sys.argv[1] == "int_pair":
+            run_int_pair()
         elif sys.argv[1] == "raise_error":
             run_raise_error()
     else:
-        print("Usage: python run_example.py <add_one|raise_error>")
+        print("Usage: python run_example.py <add_one|int_pair|raise_error>")
diff --git a/examples/packaging/src/extension.cc 
b/examples/packaging/src/extension.cc
index 8d2c504..38525c9 100644
--- a/examples/packaging/src/extension.cc
+++ b/examples/packaging/src/extension.cc
@@ -60,6 +60,38 @@ void AddOne(ffi::TensorView x, ffi::TensorView y) {
 // expose global symbol add_one
 TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, my_ffi_extension::AddOne);
 
+/*!
+ * \brief Example of a custom object that is exposed to the FFI library
+ */
+class IntPairObj : public tvm::ffi::Object {
+ public:
+  int64_t a;
+  int64_t b;
+
+  IntPairObj() = default;
+  IntPairObj(int64_t a, int64_t b) : a(a), b(b) {}
+
+  int64_t GetFirst() const { return this->a; }
+
+  // Required: declare type information
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ffi_extension.IntPair", IntPairObj, 
tvm::ffi::Object);
+};
+
+/*!
+ * \brief Defines an explicit reference to IntPairObj
+ *
+ * A reference wrapper serves as a reference-counted ptr to the object.
+ * you can use obj->field to access the fields of the object.
+ */
+class IntPair : public tvm::ffi::ObjectRef {
+ public:
+  // Constructor
+  explicit IntPair(int64_t a, int64_t b) { data_ = 
tvm::ffi::make_object<IntPairObj>(a, b); }
+
+  // Required: define object reference methods
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntPair, tvm::ffi::ObjectRef, 
IntPairObj);
+};
+
 // The static initialization block is
 // called once when the library is loaded.
 TVM_FFI_STATIC_INIT_BLOCK() {
@@ -85,5 +117,15 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   // tvm::ffi::Module::LoadFromFile, instead, just load the dll or simply 
bundle into the
   // final project
   refl::GlobalDef().def("my_ffi_extension.raise_error", RaiseError);
+  // register the object into the system
+  // register field accessors and a global static function `__ffi_init__` as 
ffi::Function
+  refl::ObjectDef<IntPairObj>()
+      .def(refl::init<int64_t, int64_t>())
+      // Example static method that returns the second element of the pair
+      .def_static("static_get_second", [](IntPair pair) -> int64_t { return 
pair->b; })
+      // Example to bind an instance method
+      .def("get_first", &IntPairObj::GetFirst)
+      .def_ro("a", &IntPairObj::a)
+      .def_ro("b", &IntPairObj::b);
 }
 }  // namespace my_ffi_extension

Reply via email to