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);