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

junrushao 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 10dc59d1 feat(c_api)!: extend TVMFFIFieldInfo setter to support 
FunctionObj dispatch (#500)
10dc59d1 is described below

commit 10dc59d196c8a88371a73a3fdc43d3400c0c0427
Author: Junru Shao <[email protected]>
AuthorDate: Tue Mar 10 10:41:32 2026 -0700

    feat(c_api)!: extend TVMFFIFieldInfo setter to support FunctionObj dispatch 
(#500)
    
    Add `kTVMFFIFieldFlagBitSetterIsFunctionObj` (bit 11) to
    `TVMFFIFieldFlags`.
    When this flag is set, the `setter` member of `TVMFFIFieldInfo` holds a
    `TVMFFIObjectHandle` pointing to a `FunctionObj`, rather than a raw
    `TVMFFIFieldSetter` function pointer. The `FunctionObj` is invoked with
    `(field_addr_as_OpaquePtr, value_as_AnyView)`.
    
    **Motivation.** The existing `TVMFFIFieldSetter` signature
    `int (*)(void*, const TVMFFIAny*)` is a static C function pointer, which
    cannot represent custom setter logic defined at runtime (e.g. a
    Python-side
    `__ffi_convert__` wrapped in a `FunctionObj`). This change lets
    reflection
    field definitions carry arbitrary callable setters while preserving full
    backward compatibility for the common case.
    
    ## Architecture
    
    - **`c_api.h`**: Change `TVMFFIFieldInfo::setter` from
    `TVMFFIFieldSetter`
      to `void*` to accommodate both the function pointer (default) and the
      `FunctionObj` handle (when the new flag is set).
    - **`reflection/accessor.h`**: Introduce `CallFieldSetter()` -- a
    central
      dispatch helper that checks the flag and either casts back to
    `TVMFFIFieldSetter` or calls `TVMFFIFunctionCall`. Uses
    value-initialization
    (`TVMFFIAny args[2]{}`, `TVMFFIAny result{}`) instead of manual zeroing.
    - **`reflection/registry.h`, `reflection/overload.h`**: Cast the
    template-
    generated `FieldSetter<T>` to `void*` when populating `TVMFFIFieldInfo`.
    - **`src/ffi/object.cc` (`TypeTable`)**: `RegisterTypeField` retains
      `FunctionObj` setters via `any_pool_` (wrapping the handle in an `Any`
      copied from a `TVMFFIAny`) so they survive in the type table without
      manual ref-count management or a custom `~Entry` destructor.
    - **`tvm_ffi_python_helpers.h`**: `SetField` gains a `field_flags`
    parameter
    and mirrors the same dispatch logic on the Cython fast-path, also using
      value-initialization for `TVMFFIAny` locals.
    
    ## Public Interfaces
    
    - **`TVMFFIFieldInfo::setter`** type changes from `TVMFFIFieldSetter` to
      `void*`. This is an ABI-breaking change for code that reads `setter`
      directly (uncommon outside the framework itself).
    - **`TVMFFIFieldFlagBitMask`** gains
    `kTVMFFIFieldFlagBitSetterIsFunctionObj
      = 1 << 11`.
    - **`TVMFFIPyCallFieldSetter`** gains a new `int64_t field_flags`
    parameter
      (Cython-internal, not user-facing).
    
    ## Behavioral Changes
    
    - All existing field setters continue to work unchanged; the new flag
      defaults to off, preserving the function-pointer path.
    - When the flag is on, setter dispatch goes through
    `TVMFFIFunctionCall`,
    which adds one level of indirection but enables runtime-defined setters.
    - `FunctionObj` setter handles are retained in the type table via
    `any_pool_`
      to prevent premature release.
    
    ## Tests
    
    Tests skipped (cherry-pick PR with review fixes only). Existing C++ and
    Python reflection tests cover the default (function pointer) path. The
    `FunctionObj` setter path will be exercised by downstream commits that
    wire
    `__ffi_convert__` through this mechanism.
    
    **BREAKING CHANGE:** `TVMFFIFieldInfo::setter` is now `void*` instead of
    `TVMFFIFieldSetter`.
---
 include/tvm/ffi/c_api.h                        | 17 +++++++++++-
 include/tvm/ffi/reflection/accessor.h          | 37 +++++++++++++++++++++++---
 include/tvm/ffi/reflection/creator.h           |  2 +-
 include/tvm/ffi/reflection/init.h              |  2 +-
 include/tvm/ffi/reflection/overload.h          |  2 +-
 include/tvm/ffi/reflection/registry.h          |  2 +-
 python/tvm_ffi/cython/base.pxi                 |  6 +++--
 python/tvm_ffi/cython/object.pxi               |  1 +
 python/tvm_ffi/cython/tvm_ffi_python_helpers.h | 27 ++++++++++++++-----
 python/tvm_ffi/cython/type_info.pxi            |  4 ++-
 rust/tvm-ffi-sys/src/c_api.rs                  | 12 ++++++---
 src/ffi/extra/reflection_extra.cc              |  3 ++-
 src/ffi/extra/serialization.cc                 |  3 ++-
 src/ffi/object.cc                              |  9 +++++++
 14 files changed, 103 insertions(+), 24 deletions(-)

diff --git a/include/tvm/ffi/c_api.h b/include/tvm/ffi/c_api.h
index 3f40bc59..0aa01579 100644
--- a/include/tvm/ffi/c_api.h
+++ b/include/tvm/ffi/c_api.h
@@ -913,6 +913,15 @@ typedef enum {
    * By default this flag is off (meaning the field accepts positional 
arguments).
    */
   kTVMFFIFieldFlagBitMaskKwOnly = 1 << 10,
+  /*!
+   * \brief The setter field is a TVMFFIObjectHandle pointing to a FunctionObj.
+   *
+   * When this flag is set, the ``setter`` member of TVMFFIFieldInfo is not a
+   * TVMFFIFieldSetter function pointer but instead a TVMFFIObjectHandle
+   * pointing to a FunctionObj.  The FunctionObj is called with two arguments:
+   * ``(field_addr_as_OpaquePtr, value_as_AnyView)``.
+   */
+  kTVMFFIFieldFlagBitSetterIsFunctionObj = 1 << 11,
 #ifdef __cplusplus
 };
 #else
@@ -1008,9 +1017,15 @@ typedef struct {
   TVMFFIFieldGetter getter;
   /*!
    * \brief The setter to access the field.
+   *
+   * When kTVMFFIFieldFlagBitSetterIsFunctionObj is NOT set (default),
+   * this is a TVMFFIFieldSetter function pointer (cast to void*).
+   * When kTVMFFIFieldFlagBitSetterIsFunctionObj IS set,
+   * this is a TVMFFIObjectHandle pointing to a FunctionObj.
+   *
    * \note The setter is set even if the field is readonly for serialization.
    */
-  TVMFFIFieldSetter setter;
+  void* setter;
   /*!
    * \brief The default value or default factory of the field.
    *
diff --git a/include/tvm/ffi/reflection/accessor.h 
b/include/tvm/ffi/reflection/accessor.h
index 700f7b8c..eb456efc 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -52,6 +52,35 @@ inline const TVMFFIFieldInfo* GetFieldInfo(std::string_view 
type_key, const char
   TVM_FFI_UNREACHABLE();
 }
 
+/*!
+ * \brief Call the field setter, dispatching between function pointer and 
FunctionObj.
+ *
+ * When kTVMFFIFieldFlagBitSetterIsFunctionObj is off, invokes the setter as a
+ * TVMFFIFieldSetter function pointer.  When on, calls via TVMFFIFunctionCall
+ * with arguments (field_addr as OpaquePtr, value).
+ *
+ * \param field_info The field info containing the setter.
+ * \param field_addr The address of the field in the object.
+ * \param value The value to set (as a TVMFFIAny pointer).
+ * \return 0 on success, nonzero on failure.
+ */
+inline int CallFieldSetter(const TVMFFIFieldInfo* field_info, void* field_addr,
+                           const TVMFFIAny* value) {
+  if (!(field_info->flags & kTVMFFIFieldFlagBitSetterIsFunctionObj)) {
+    auto setter = reinterpret_cast<TVMFFIFieldSetter>(field_info->setter);
+    return setter(field_addr, value);
+  } else {
+    TVMFFIAny args[2]{};
+    args[0].type_index = kTVMFFIOpaquePtr;
+    args[0].v_ptr = field_addr;
+    args[1] = *value;
+    TVMFFIAny result{};
+    result.type_index = kTVMFFINone;
+    return 
TVMFFIFunctionCall(static_cast<TVMFFIObjectHandle>(field_info->setter), args, 2,
+                              &result);
+  }
+}
+
 /*!
  * \brief helper wrapper class to obtain a getter.
  */
@@ -118,8 +147,8 @@ class FieldSetter {
    */
   void operator()(const Object* obj_ptr, AnyView value) const {
     const void* addr = reinterpret_cast<const char*>(obj_ptr) + 
field_info_->offset;
-    TVM_FFI_CHECK_SAFE_CALL(
-        field_info_->setter(const_cast<void*>(addr), reinterpret_cast<const 
TVMFFIAny*>(&value)));
+    TVM_FFI_CHECK_SAFE_CALL(CallFieldSetter(field_info_, 
const_cast<void*>(addr),
+                                            reinterpret_cast<const 
TVMFFIAny*>(&value)));
   }
 
   void operator()(const ObjectPtr<Object>& obj_ptr, AnyView value) const {
@@ -215,9 +244,9 @@ inline void SetFieldToDefault(const TVMFFIFieldInfo* 
field_info, void* field_add
     Function factory =
         
AnyView::CopyFromTVMFFIAny(field_info->default_value_or_factory).cast<Function>();
     Any default_val = factory();
-    field_info->setter(field_addr, reinterpret_cast<const 
TVMFFIAny*>(&default_val));
+    CallFieldSetter(field_info, field_addr, reinterpret_cast<const 
TVMFFIAny*>(&default_val));
   } else {
-    field_info->setter(field_addr, &(field_info->default_value_or_factory));
+    CallFieldSetter(field_info, field_addr, 
&(field_info->default_value_or_factory));
   }
 }
 
diff --git a/include/tvm/ffi/reflection/creator.h 
b/include/tvm/ffi/reflection/creator.h
index a7e860c1..300ad512 100644
--- a/include/tvm/ffi/reflection/creator.h
+++ b/include/tvm/ffi/reflection/creator.h
@@ -76,7 +76,7 @@ class ObjectCreator {
       void* field_addr = reinterpret_cast<char*>(ptr.get()) + 
field_info->offset;
       if (fields.count(field_name) != 0) {
         Any field_value = fields[field_name];
-        field_info->setter(field_addr, reinterpret_cast<const 
TVMFFIAny*>(&field_value));
+        CallFieldSetter(field_info, field_addr, reinterpret_cast<const 
TVMFFIAny*>(&field_value));
         ++match_field_count;
       } else if (field_info->flags & kTVMFFIFieldFlagBitMaskHasDefault) {
         SetFieldToDefault(field_info, field_addr);
diff --git a/include/tvm/ffi/reflection/init.h 
b/include/tvm/ffi/reflection/init.h
index 1a1aa21f..337753ce 100644
--- a/include/tvm/ffi/reflection/init.h
+++ b/include/tvm/ffi/reflection/init.h
@@ -128,7 +128,7 @@ inline Function MakeInit(int32_t type_index) {
 
         auto set_field = [&](size_t fi, const TVMFFIAny* value) {
           void* addr = reinterpret_cast<char*>(obj_ptr.get()) + 
info->all_fields[fi].info->offset;
-          TVM_FFI_CHECK_SAFE_CALL(info->all_fields[fi].info->setter(addr, 
value));
+          TVM_FFI_CHECK_SAFE_CALL(CallFieldSetter(info->all_fields[fi].info, 
addr, value));
           field_set[fi] = true;
         };
 
diff --git a/include/tvm/ffi/reflection/overload.h 
b/include/tvm/ffi/reflection/overload.h
index 6d0e783d..8647c3c6 100644
--- a/include/tvm/ffi/reflection/overload.h
+++ b/include/tvm/ffi/reflection/overload.h
@@ -448,7 +448,7 @@ class OverloadObjectDef : private ObjectDef<Class> {
       info.flags |= kTVMFFIFieldFlagBitMaskWritable;
     }
     info.getter = ReflectionDefBase::FieldGetter<T>;
-    info.setter = ReflectionDefBase::FieldSetter<T>;
+    info.setter = reinterpret_cast<void*>(ReflectionDefBase::FieldSetter<T>);
     // initialize default value to nullptr
     info.default_value_or_factory = AnyView(nullptr).CopyToTVMFFIAny();
     info.doc = TVMFFIByteArray{nullptr, 0};
diff --git a/include/tvm/ffi/reflection/registry.h 
b/include/tvm/ffi/reflection/registry.h
index 20760155..719d6121 100644
--- a/include/tvm/ffi/reflection/registry.h
+++ b/include/tvm/ffi/reflection/registry.h
@@ -911,7 +911,7 @@ class ObjectDef : public ReflectionDefBase {
       info.flags |= kTVMFFIFieldFlagBitMaskWritable;
     }
     info.getter = FieldGetter<T>;
-    info.setter = FieldSetter<T>;
+    info.setter = reinterpret_cast<void*>(FieldSetter<T>);
     // initialize default value to nullptr
     info.default_value_or_factory = AnyView(nullptr).CopyToTVMFFIAny();
     info.doc = TVMFFIByteArray{nullptr, 0};
diff --git a/python/tvm_ffi/cython/base.pxi b/python/tvm_ffi/cython/base.pxi
index 15fc053e..9c1d4b4d 100644
--- a/python/tvm_ffi/cython/base.pxi
+++ b/python/tvm_ffi/cython/base.pxi
@@ -207,6 +207,7 @@ cdef extern from "tvm/ffi/c_api.h":
         kTVMFFIFieldFlagBitMaskDefaultFromFactory = 1 << 5
         kTVMFFIFieldFlagBitMaskInitOff = 1 << 9
         kTVMFFIFieldFlagBitMaskKwOnly = 1 << 10
+        kTVMFFIFieldFlagBitSetterIsFunctionObj = 1 << 11
 
     ctypedef int (*TVMFFIFieldGetter)(void* field, TVMFFIAny* result) noexcept
     ctypedef int (*TVMFFIFieldSetter)(void* field, const TVMFFIAny* value) 
noexcept
@@ -221,7 +222,7 @@ cdef extern from "tvm/ffi/c_api.h":
         int64_t alignment
         int64_t offset
         TVMFFIFieldGetter getter
-        TVMFFIFieldSetter setter
+        void* setter
         TVMFFIAny default_value_or_factory
         int32_t field_static_type_index
 
@@ -357,7 +358,8 @@ cdef extern from "tvm_ffi_python_helpers.h":
 
     int TVMFFIPyCallFieldSetter(
         TVMFFIPyArgSetterFactory setter_factory,
-        TVMFFIFieldSetter field_setter,
+        void* field_setter,
+        int64_t field_flags,
         void* field_ptr,
         PyObject* py_arg,
         int* c_api_ret_code
diff --git a/python/tvm_ffi/cython/object.pxi b/python/tvm_ffi/cython/object.pxi
index b02c95e0..ecdceac7 100644
--- a/python/tvm_ffi/cython/object.pxi
+++ b/python/tvm_ffi/cython/object.pxi
@@ -488,6 +488,7 @@ cdef _type_info_create_from_type_key(object type_cls, str 
type_key):
         setter = FieldSetter.__new__(FieldSetter)
         (<FieldSetter>setter).setter = field.setter
         (<FieldSetter>setter).offset = field.offset
+        (<FieldSetter>setter).flags = field.flags
         metadata_obj = json.loads(bytearray_to_str(&field.metadata)) if 
field.metadata.size != 0 else {}
         fields.append(
             TypeField(
diff --git a/python/tvm_ffi/cython/tvm_ffi_python_helpers.h 
b/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
index 666bb6e9..20152159 100644
--- a/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
+++ b/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
@@ -398,14 +398,26 @@ class TVMFFIPyCallManager {
     }
   }
 
-  TVM_FFI_INLINE int SetField(TVMFFIPyArgSetterFactory setter_factory,
-                              TVMFFIFieldSetter field_setter, void* field_ptr, 
PyObject* py_arg,
+  TVM_FFI_INLINE int SetField(TVMFFIPyArgSetterFactory setter_factory, void* 
field_setter,
+                              int64_t field_flags, void* field_ptr, PyObject* 
py_arg,
                               int* c_api_ret_code) {
     try {
       TVMFFIPyCallContext ctx(&call_stack_, 1);
       TVMFFIAny* c_arg = ctx.packed_args;
       if (SetArgument(setter_factory, &ctx, py_arg, c_arg) != 0) return -1;
-      c_api_ret_code[0] = (*field_setter)(field_ptr, c_arg);
+      if (!(field_flags & kTVMFFIFieldFlagBitSetterIsFunctionObj)) {
+        auto setter = reinterpret_cast<TVMFFIFieldSetter>(field_setter);
+        c_api_ret_code[0] = (*setter)(field_ptr, c_arg);
+      } else {
+        TVMFFIAny args[2]{};
+        args[0].type_index = kTVMFFIOpaquePtr;
+        args[0].v_ptr = field_ptr;
+        args[1] = *c_arg;
+        TVMFFIAny result{};
+        result.type_index = kTVMFFINone;
+        c_api_ret_code[0] =
+            TVMFFIFunctionCall(static_cast<TVMFFIObjectHandle>(field_setter), 
args, 2, &result);
+      }
       return 0;
     } catch (const std::exception& ex) {
       // very rare, catch c++ exception and set python error
@@ -534,17 +546,18 @@ TVM_FFI_INLINE int 
TVMFFIPyConstructorCall(TVMFFIPyArgSetterFactory setter_facto
 /*!
  * \brief Set a field of a FFI object
  * \param setter_factory The factory function to create the setter
- * \param field_setter The field setter function
+ * \param field_setter The field setter (function pointer or FunctionObj 
handle)
+ * \param field_flags The field flags (to dispatch between function pointer 
and FunctionObj)
  * \param field_ptr The pointer to the field
  * \param py_arg The python argument to be set
  * \param c_api_ret_code The return code of the function
  * \return 0 on success, nonzero on failure
  */
 TVM_FFI_INLINE int TVMFFIPyCallFieldSetter(TVMFFIPyArgSetterFactory 
setter_factory,
-                                           TVMFFIFieldSetter field_setter, 
void* field_ptr,
+                                           void* field_setter, int64_t 
field_flags, void* field_ptr,
                                            PyObject* py_arg, int* 
c_api_ret_code) {
-  return TVMFFIPyCallManager::ThreadLocal()->SetField(setter_factory, 
field_setter, field_ptr,
-                                                      py_arg, c_api_ret_code);
+  return TVMFFIPyCallManager::ThreadLocal()->SetField(setter_factory, 
field_setter, field_flags,
+                                                      field_ptr, py_arg, 
c_api_ret_code);
 }
 
 /*!
diff --git a/python/tvm_ffi/cython/type_info.pxi 
b/python/tvm_ffi/cython/type_info.pxi
index ab4cdc9b..28fa4701 100644
--- a/python/tvm_ffi/cython/type_info.pxi
+++ b/python/tvm_ffi/cython/type_info.pxi
@@ -38,8 +38,9 @@ cdef class FieldGetter:
 
 cdef class FieldSetter:
     cdef dict __dict__
-    cdef TVMFFIFieldSetter setter
+    cdef void* setter
     cdef int64_t offset
+    cdef int64_t flags
 
     def __call__(self, CObject obj, value):
         cdef int c_api_ret_code
@@ -47,6 +48,7 @@ cdef class FieldSetter:
         TVMFFIPyCallFieldSetter(
             TVMFFIPyArgSetterFactory_,
             self.setter,
+            self.flags,
             field_ptr,
             <PyObject*>value,
             &c_api_ret_code
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 2035eda9..56209bfa 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -283,9 +283,15 @@ pub struct TVMFFIFieldInfo {
     pub offset: i64,
     /// The getter to access the field
     pub getter: Option<TVMFFIFieldGetter>,
-    /// The setter to access the field
-    /// The setter is set even if the field is readonly for serialization
-    pub setter: Option<TVMFFIFieldSetter>,
+    /// The setter to access the field.
+    ///
+    /// When kTVMFFIFieldFlagBitSetterIsFunctionObj is NOT set (default),
+    /// this is a TVMFFIFieldSetter function pointer cast to *mut c_void.
+    /// When kTVMFFIFieldFlagBitSetterIsFunctionObj IS set,
+    /// this is a TVMFFIObjectHandle pointing to a FunctionObj.
+    ///
+    /// The setter is set even if the field is readonly for serialization.
+    pub setter: *mut c_void,
     /// The default value or factory of the field, this field holds AnyView.
     /// Valid when flags set kTVMFFIFieldFlagBitMaskHasDefault.
     /// When kTVMFFIFieldFlagBitMaskDefaultFromFactory is also set,
diff --git a/src/ffi/extra/reflection_extra.cc 
b/src/ffi/extra/reflection_extra.cc
index 5182f1df..b5ced5c2 100644
--- a/src/ffi/extra/reflection_extra.cc
+++ b/src/ffi/extra/reflection_extra.cc
@@ -77,7 +77,8 @@ void MakeObjectFromPackedArgs(ffi::PackedArgs args, Any* ret) 
{
       void* field_addr = reinterpret_cast<char*>(ptr.get()) + 
field_info->offset;
       if (arg_index < keys.size()) {
         AnyView field_value = args[static_cast<int>(arg_index * 2 + 2)];
-        field_info->setter(field_addr, reinterpret_cast<const 
TVMFFIAny*>(&field_value));
+        reflection::CallFieldSetter(field_info, field_addr,
+                                    reinterpret_cast<const 
TVMFFIAny*>(&field_value));
         keys_found[arg_index] = true;
       } else if (field_info->flags & kTVMFFIFieldFlagBitMaskHasDefault) {
         reflection::SetFieldToDefault(field_info, field_addr);
diff --git a/src/ffi/extra/serialization.cc b/src/ffi/extra/serialization.cc
index 2351f7ca..80b96ec7 100644
--- a/src/ffi/extra/serialization.cc
+++ b/src/ffi/extra/serialization.cc
@@ -411,7 +411,8 @@ class ObjectGraphDeserializer {
       void* field_addr = reinterpret_cast<char*>(ptr.get()) + 
field_info->offset;
       if (data_object.count(field_name) != 0) {
         Any field_value = decode_field_value(field_info, 
data_object[field_name]);
-        field_info->setter(field_addr, reinterpret_cast<const 
TVMFFIAny*>(&field_value));
+        reflection::CallFieldSetter(field_info, field_addr,
+                                    reinterpret_cast<const 
TVMFFIAny*>(&field_value));
       } else if (field_info->flags & kTVMFFIFieldFlagBitMaskHasDefault) {
         reflection::SetFieldToDefault(field_info, field_addr);
       } else {
diff --git a/src/ffi/object.cc b/src/ffi/object.cc
index 94ad9815..011af14e 100644
--- a/src/ffi/object.cc
+++ b/src/ffi/object.cc
@@ -216,6 +216,15 @@ class TypeTable {
   void RegisterTypeField(int32_t type_index, const TVMFFIFieldInfo* info) {
     Entry* entry = GetTypeEntry(type_index);
     TVMFFIFieldInfo field_data = *info;
+    // Retain FunctionObj setter via any_pool_ so it outlives the Entry.
+    if ((field_data.flags & kTVMFFIFieldFlagBitSetterIsFunctionObj) &&
+        field_data.setter != nullptr) {
+      TVMFFIAny setter_ref;
+      setter_ref.type_index = kTVMFFIFunction;
+      setter_ref.zero_padding = 0;
+      setter_ref.v_obj = static_cast<TVMFFIObject*>(field_data.setter);
+      any_pool_.emplace_back(AnyView::CopyFromTVMFFIAny(setter_ref));
+    }
     field_data.name = this->CopyString(info->name);
     field_data.doc = this->CopyString(info->doc);
     field_data.metadata = this->CopyString(info->metadata);

Reply via email to