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 ac7bf68 feat: Embed metadata for FFI exported functions (#230)
ac7bf68 is described below
commit ac7bf68058b392c3a8475619016fde48bd08334b
Author: Kathryn (Jinqi) Chen <[email protected]>
AuthorDate: Thu Nov 20 17:40:59 2025 -0800
feat: Embed metadata for FFI exported functions (#230)
## Summary
This PR adds reflection metadata support for functions exported via
`TVM_FFI_DLL_EXPORT_TYPED_FUNC`, enabling signature validation and
memory effect analysis without invoking the function. This addresses the
need for compiler frameworks (e.g., JAX/XLA, MLIR-based compilers) to
validate third-party FFI modules at compile time.
## Core Changes
**1. `TVM_FFI_DLL_EXPORT_TYPED_FUNC` macro**
- Exports two symbols per function:
- `__tvm_ffi_<name>`
- `__tvm_ffi__metadata_<name>`
**Metadata format:**
```
{
"type_schema": "{\"type\":\"ffi.Function\",\"args\":[...]}",
}
```
Based on discussion, we choose to not include C++ const information in
metadata in this PR, and think about it as a followup.
**2. `TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC` macro**
- Exports three symbols per function:
- `__tvm_ffi_<name>`
- `__tvm_ffi__metadata_<name>`
- `__tvm_ffi__doc_<name>`
**3. Module API** (C++)
- `Module::GetFunctionMetadata(name)` - Query metadata from loaded
modules
- `Module::GetFunctionDoc(name)` - Query documentation
- Works with both static and dynamic linking
**4. Module API** (Python)
- `mod.get_function_metadata("func_name")` - Query metadata from loaded
modules
- `mod.get_function_doc("func_name")` - Query documentation
- Might be good to keep methods private
## Tests
The tests include both unit tests and integration tests.
**C++ Tests:**
- `tests/cpp/test_metadata.cc` - Direct symbol tests
- `tests/cpp/test_function.cc` - Static linking scenario
- `tests/cpp/extra/test_module.cc` - Module API
**Python Tests:**
- `tests/python/test_metadata.py` - Unit test
- `tests/python/test_build.py` - Integration test
---
docs/get_started/quickstart.rst | 3 +-
docs/guides/compiler_integration.md | 10 +-
docs/guides/cpp_guide.md | 47 +++++-
include/tvm/ffi/extra/module.h | 31 +++-
include/tvm/ffi/function.h | 131 ++++++++++++++---
python/tvm_ffi/cpp/extension.py | 46 ++++--
python/tvm_ffi/module.py | 101 ++++++++++++-
src/ffi/extra/library_module.cc | 24 ++++
src/ffi/testing/testing.cc | 190 ++++++++++++++++++-------
tests/cpp/test_function.cc | 15 ++
tests/cpp/test_metadata.cc | 42 ++++++
tests/python/test_build.cc | 2 +
tests/python/test_build.py | 276 ++++++++++++++++++++++++++++++++++++
13 files changed, 822 insertions(+), 96 deletions(-)
diff --git a/docs/get_started/quickstart.rst b/docs/get_started/quickstart.rst
index 650b750..1ebb533 100644
--- a/docs/get_started/quickstart.rst
+++ b/docs/get_started/quickstart.rst
@@ -76,7 +76,8 @@ Suppose we implement a C++ function ``AddOne`` that performs
elementwise ``y = x
The macro :c:macro:`TVM_FFI_DLL_EXPORT_TYPED_FUNC` exports the C++ function
``AddOne``
-as a TVM FFI compatible symbol with the name ``__tvm_ffi_add_one_cpu/cuda`` in
the resulting library.
+as a TVM FFI compatible symbol ``__tvm_ffi_add_one_cpu/cuda``. If
:c:macro:`TVM_FFI_DLL_EXPORT_INCLUDE_METADATA` is set to 1,
+it also exports the function's metadata as a symbol
``__tvm_ffi__metadata_add_one_cpu/cuda`` for type checking and stub generation.
The class :cpp:class:`tvm::ffi::TensorView` allows zero-copy interop with
tensors from different ML frameworks:
diff --git a/docs/guides/compiler_integration.md
b/docs/guides/compiler_integration.md
index aff6f90..254df38 100644
--- a/docs/guides/compiler_integration.md
+++ b/docs/guides/compiler_integration.md
@@ -30,9 +30,13 @@ following options:
- For compilers that generate host functions via codegen (e.g., LLVM), one can
generate the symbol `__tvm_ffi_<func_name>`, where `<funcname>` is the
exported
- function.
-- For kernel generators that generate C++ host code, one can directly
- use {c:macro}`TVM_FFI_DLL_EXPORT_TYPED_FUNC` to expose the symbol.
+ function. Optionally, also generate `__tvm_ffi__metadata_<func_name>` for
reflection.
+- For kernel generators that generate C++ host code, use
{c:macro}`TVM_FFI_DLL_EXPORT_TYPED_FUNC`.
+ This macro automatically exports function metadata when
{c:macro}`TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`
+ is set to 1.
+- To export documentation strings, use
{c:macro}`TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC` separately
+ after exporting the function. This enhances tooling support (stub
generation, IDE tooltips).
+ Documentation export is also controlled by
{c:macro}`TVM_FFI_DLL_EXPORT_INCLUDE_METADATA`.
The following code snippet shows C code that corresponds to a
function performing `add_one_c` under the ABI. It is reasonably
straightforward for
diff --git a/docs/guides/cpp_guide.md b/docs/guides/cpp_guide.md
index ef69ed3..e5f09b7 100644
--- a/docs/guides/cpp_guide.md
+++ b/docs/guides/cpp_guide.md
@@ -292,12 +292,55 @@ void AddOne(DLTensor* x, DLTensor* y) {
TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, my_ffi_extension::AddOne);
```
-The new `add_one` takes the signature of `TVMFFISafeCallType` and can be
wrapped as `ffi::Function`
-through the C++ `ffi::Module` API.
+The new `add_one` takes the signature of `TVMFFISafeCallType`, and can loaded
and queried through the C++ `ffi::Module` API.
+
+When flag `TVM_FFI_DLL_EXPORT_TYPED_FUNC_METADATA` is on, the macro exports
both the function and its type metadata, enabling
+signature validation without calling the function.
+
+The metadata contains:
+
+- **type_schema**: JSON string describing function signature (return type and
argument types)
```cpp
ffi::Module mod = ffi::Module::LoadFromFile("path/to/export_lib.so");
+
+// Get the function
ffi::Function func = mod->GetFunction("add_one").value();
+
+// Query metadata (type schema information)
+ffi::Optional<ffi::String> metadata = mod->GetFunctionMetadata("add_one");
+if (metadata.has_value()) {
+ // Parse JSON metadata for validation
+ // Contains: {"type_schema": "..."}
+}
+```
+
+For functions that need documentation, use the
`TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC` macro separately:
+
+```cpp
+#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 1
+
+void ProcessBatch(ffi::TensorView input, ffi::TensorView output) {
+ // ... implementation
+}
+
+// Export the function
+TVM_FFI_DLL_EXPORT_TYPED_FUNC(process_batch, ProcessBatch);
+
+// Export documentation separately (make sure
TVM_FFI_DLL_EXPORT_INCLUDE_METADATA is set to 1)
+TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(
+ process_batch,
+ R"(Process a batch of inputs and write results to output tensor.
+
+Parameters
+----------
+input : TensorView
+ Input tensor to process
+output : TensorView
+ Output tensor for results)");
+
+// Query documentation
+ffi::Optional<ffi::String> doc = mod->GetFunctionDoc("process_batch");
```
## Error Handling
diff --git a/include/tvm/ffi/extra/module.h b/include/tvm/ffi/extra/module.h
index ea6cf3f..6af26c2 100644
--- a/include/tvm/ffi/extra/module.h
+++ b/include/tvm/ffi/extra/module.h
@@ -74,7 +74,9 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
/*!
* \brief Get the docstring of the function, if available.
* \param name The name of the function.
- * \return The docstring of the function.
+ * \return The documentation string if available, nullopt otherwise.
+ *
+ * \sa GetFunctionMetadata, TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC
*/
virtual Optional<String> GetFunctionDoc(const String& name) { return
std::nullopt; }
// Rationale: We separate the docstring from the metadata since docstrings
@@ -83,7 +85,18 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
/*!
* \brief Get the metadata of the function, if available.
* \param name The name of the function.
- * \return The metadata stored in json string format.
+ * \return The metadata as JSON string if available, nullopt otherwise.
+ *
+ * \code
+ * Module mod = Module::LoadFromFile("lib.so");
+ * Optional<String> metadata = mod->GetFunctionMetadata("my_func");
+ * if (metadata.has_value()) {
+ * // Parse JSON: {"type_schema": "..."}
+ * validate_signature(*metadata);
+ * }
+ * \endcode
+ *
+ * \sa GetFunctionDoc, TVM_FFI_DLL_EXPORT_TYPED_FUNC
*/
virtual Optional<String> GetFunctionMetadata(const String& name) { return
std::nullopt; }
/*!
@@ -142,15 +155,19 @@ class TVM_FFI_EXTRA_CXX_API ModuleObj : public Object {
/*!
* \brief Get the function docstring of the function if available.
* \param name The name of the function.
- * \param query_imports Whether to query imported modules.
- * \return The function docstring of the function.
+ * \param query_imports Whether to also query modules imported by this
module.
+ * \return The documentation string if available, nullopt otherwise.
+ *
+ * \sa GetFunctionMetadata
*/
Optional<String> GetFunctionDoc(const String& name, bool query_imports);
/*!
* \brief Get the function metadata of the function if available.
* \param name The name of the function.
- * \param query_imports Whether to query imported modules.
- * \return The function metadata of the function in json format.
+ * \param query_imports Whether to also query modules imported by this
module.
+ * \return The metadata as JSON string if available, nullopt otherwise.
+ *
+ * \sa GetFunctionDoc
*/
Optional<String> GetFunctionMetadata(const String& name, bool query_imports);
/*!
@@ -275,6 +292,8 @@ constexpr const char* tvm_ffi_library_ctx =
"__tvm_ffi__library_ctx";
constexpr const char* tvm_ffi_library_bin = "__tvm_ffi__library_bin";
/*! \brief Optional metadata prefix of a symbol. */
constexpr const char* tvm_ffi_metadata_prefix = "__tvm_ffi__metadata_";
+/*! \brief Optional documentation prefix of a symbol. */
+constexpr const char* tvm_ffi_doc_prefix = "__tvm_ffi__doc_";
} // namespace symbol
} // namespace ffi
} // namespace tvm
diff --git a/include/tvm/ffi/function.h b/include/tvm/ffi/function.h
index b339c8c..3f6fe9b 100644
--- a/include/tvm/ffi/function.h
+++ b/include/tvm/ffi/function.h
@@ -23,6 +23,16 @@
#ifndef TVM_FFI_FUNCTION_H_
#define TVM_FFI_FUNCTION_H_
+/*!
+ * \brief Controls whether DLL exports should include metadata.
+ *
+ * When set to 1, exported functions will include additional metadata.
+ * When set to 0 (default), exports are minimal without metadata.
+ */
+#ifndef TVM_FFI_DLL_EXPORT_INCLUDE_METADATA
+#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 0
+#endif
+
#include <tvm/ffi/any.h>
#include <tvm/ffi/base_details.h>
#include <tvm/ffi/c_api.h>
@@ -30,6 +40,7 @@
#include <tvm/ffi/function_details.h>
#include <functional>
+#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
@@ -858,13 +869,37 @@ inline int32_t TypeKeyToIndex(std::string_view type_key) {
return type_index;
}
+/// \cond Doxygen_Suppress
+// Internal implementation macros used by TVM_FFI_DLL_EXPORT_TYPED_FUNC and
related macros.
+// These should not be used directly; use the public macros instead.
+
+// Internal implementation macro that generates the C ABI wrapper function
+#define TVM_FFI_DLL_EXPORT_TYPED_FUNC_IMPL_(ExportName, Function)
\
+ extern "C" {
\
+ TVM_FFI_DLL_EXPORT int __tvm_ffi_##ExportName(void* self, const TVMFFIAny*
args, \
+ int32_t num_args, TVMFFIAny*
result) { \
+ TVM_FFI_SAFE_CALL_BEGIN();
\
+ using FuncInfo = ::tvm::ffi::details::FunctionInfo<decltype(Function)>;
\
+ static std::string name = #ExportName;
\
+ ::tvm::ffi::details::unpack_call<typename FuncInfo::RetType>(
\
+ std::make_index_sequence<FuncInfo::num_args>{}, &name, Function,
\
+ reinterpret_cast<const ::tvm::ffi::AnyView*>(args), num_args,
\
+ reinterpret_cast<::tvm::ffi::Any*>(result));
\
+ TVM_FFI_SAFE_CALL_END();
\
+ }
\
+ }
+/// \endcond
+
/*!
* \brief Export typed function as a SafeCallType symbol that follows the FFI
ABI.
*
+ * This macro exports the function and automatically exports metadata when
+ * TVM_FFI_DLL_EXPORT_INCLUDE_METADATA is defined.
+ *
* \param ExportName The symbol name to be exported.
* \param Function The typed function.
*
- * \sa ffi::TypedFunction
+ * \sa ffi::TypedFunction, TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC
*
* \code
*
@@ -881,22 +916,86 @@ inline int32_t TypeKeyToIndex(std::string_view type_key) {
* });
* \endcode
*
- * \note The final symbol name is `__tvm_ffi_<ExportName>`.
+ * \note The final symbol names are:
+ * - `__tvm_ffi_<ExportName>` (function)
+ * - `__tvm_ffi__metadata_<ExportName>` (metadata - only when
+ * TVM_FFI_DLL_EXPORT_INCLUDE_METADATA is defined)
*/
-#define TVM_FFI_DLL_EXPORT_TYPED_FUNC(ExportName, Function)
\
- extern "C" {
\
- TVM_FFI_DLL_EXPORT int __tvm_ffi_##ExportName(void* self, const TVMFFIAny*
args, \
- int32_t num_args, TVMFFIAny*
result) { \
- TVM_FFI_SAFE_CALL_BEGIN();
\
- using FuncInfo = ::tvm::ffi::details::FunctionInfo<decltype(Function)>;
\
- static std::string name = #ExportName;
\
- ::tvm::ffi::details::unpack_call<typename FuncInfo::RetType>(
\
- std::make_index_sequence<FuncInfo::num_args>{}, &name, Function,
\
- reinterpret_cast<const ::tvm::ffi::AnyView*>(args), num_args,
\
- reinterpret_cast<::tvm::ffi::Any*>(result));
\
- TVM_FFI_SAFE_CALL_END();
\
- }
\
- }
+#if TVM_FFI_DLL_EXPORT_INCLUDE_METADATA
+#define TVM_FFI_DLL_EXPORT_TYPED_FUNC(ExportName, Function)
\
+ TVM_FFI_DLL_EXPORT_TYPED_FUNC_IMPL_(ExportName, Function)
\
+ extern "C" {
\
+ TVM_FFI_DLL_EXPORT int __tvm_ffi__metadata_##ExportName(void* self, const
TVMFFIAny* args, \
+ int32_t num_args,
TVMFFIAny* result) { \
+ TVM_FFI_SAFE_CALL_BEGIN();
\
+ using FuncInfo = ::tvm::ffi::details::FunctionInfo<decltype(Function)>;
\
+ std::ostringstream os;
\
+ os << R"({"type_schema":)"
\
+ << ::tvm::ffi::EscapeString(::tvm::ffi::String(FuncInfo::TypeSchema()))
<< R"(})"; \
+ ::tvm::ffi::String str(os.str());
\
+ ::tvm::ffi::TypeTraits<::tvm::ffi::String>::MoveToAny(std::move(str),
result); \
+ TVM_FFI_SAFE_CALL_END();
\
+ }
\
+ }
+#else
+#define TVM_FFI_DLL_EXPORT_TYPED_FUNC(ExportName, Function) \
+ TVM_FFI_DLL_EXPORT_TYPED_FUNC_IMPL_(ExportName, Function)
+#endif
+
+/*!
+ * \brief Export documentation string for a typed function.
+ *
+ * This macro exports a documentation string associated with a function export
name.
+ * The docstring can be used by stub generators and documentation tools.
+ * This macro only exports the docstring; it does not export the function
itself.
+ *
+ * \param ExportName The symbol name that the docstring is associated with.
+ * \param DocString The documentation string (C string literal).
+ *
+ * \sa ffi::TypedFunction, TVM_FFI_DLL_EXPORT_TYPED_FUNC
+ *
+ * \code
+ *
+ * int Add(int a, int b) {
+ * return a + b;
+ * }
+ *
+ * TVM_FFI_DLL_EXPORT_TYPED_FUNC(add, Add);
+ * TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(
+ * add,
+ * R"(Add two integers and return the sum.
+ *
+ * Parameters
+ * ----------
+ * a : int
+ * First integer
+ * b : int
+ * Second integer
+ *
+ * Returns
+ * -------
+ * result : int
+ * Sum of a and b)");
+ *
+ * \endcode
+ *
+ * \note The exported symbol name is `__tvm_ffi__doc_<ExportName>` (docstring
getter function).
+ * This symbol is only exported when TVM_FFI_DLL_EXPORT_INCLUDE_METADATA
is defined.
+ */
+#if TVM_FFI_DLL_EXPORT_INCLUDE_METADATA
+#define TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(ExportName, DocString)
\
+ extern "C" {
\
+ TVM_FFI_DLL_EXPORT int __tvm_ffi__doc_##ExportName(void* self, const
TVMFFIAny* args, \
+ int32_t num_args,
TVMFFIAny* result) { \
+ TVM_FFI_SAFE_CALL_BEGIN();
\
+ ::tvm::ffi::String str(DocString);
\
+ ::tvm::ffi::TypeTraits<::tvm::ffi::String>::MoveToAny(std::move(str),
result); \
+ TVM_FFI_SAFE_CALL_END();
\
+ }
\
+ }
+#else
+#define TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(ExportName, DocString)
+#endif
} // namespace ffi
} // namespace tvm
#endif // TVM_FFI_FUNCTION_H_
diff --git a/python/tvm_ffi/cpp/extension.py b/python/tvm_ffi/cpp/extension.py
index ff4daff..4b1af8f 100644
--- a/python/tvm_ffi/cpp/extension.py
+++ b/python/tvm_ffi/cpp/extension.py
@@ -353,6 +353,23 @@ def build_ninja(build_dir: str) -> None:
raise RuntimeError("\n".join(msg))
+# Translation table for escaping C++ string literals
+_CPP_ESCAPE_TABLE = str.maketrans(
+ {
+ "\\": "\\\\",
+ '"': '\\"',
+ "\n": "\\n",
+ "\r": "\\r",
+ "\t": "\\t",
+ }
+)
+
+
+def _escape_cpp_string_literal(s: str) -> str:
+ """Escape special characters for C++ string literals."""
+ return s.translate(_CPP_ESCAPE_TABLE)
+
+
def _decorate_with_tvm_ffi(source: str, functions: Mapping[str, str]) -> str:
"""Decorate the given source code with TVM FFI export macros."""
sources = [
@@ -367,7 +384,11 @@ def _decorate_with_tvm_ffi(source: str, functions:
Mapping[str, str]) -> str:
for func_name, func_doc in functions.items():
sources.append(f"TVM_FFI_DLL_EXPORT_TYPED_FUNC({func_name},
{func_name});")
- _ = func_doc # todo: add support to embed function docstring to the
tvm ffi functions.
+
+ if func_doc:
+ # Escape the docstring for C++ string literal
+ escaped_doc = _escape_cpp_string_literal(func_doc)
+ sources.append(f'TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC({func_name},
"{escaped_doc}");')
sources.append("")
@@ -496,11 +517,12 @@ def build_inline(
The CUDA source code. It can be a list of sources or a single source.
functions
The functions in cpp_sources or cuda_source that will be exported to
the tvm ffi module. When a mapping is
- given, the keys are the names of the exported functions, and the
values are docstrings for the functions. When
- a sequence or a single string is given, they are the functions needed
to be exported, and the docstrings are set
- to empty strings. A single function name can also be given as a
string. When cpp_sources is given, the functions
- must be declared (not necessarily defined) in the cpp_sources. When
cpp_sources is not given, the functions
- must be defined in the cuda_sources. If not specified, no function
will be exported.
+ given, the keys are the names of the exported functions, and the
values are docstrings for the functions
+ (use an empty string to skip documentation for specific functions).
When a sequence or a single string is given, they are
+ the functions needed to be exported, and the docstrings are set to
empty strings. A single function name can
+ also be given as a string. When cpp_sources is given, the functions
must be declared (not necessarily defined)
+ in the cpp_sources. When cpp_sources is not given, the functions must
be defined in the cuda_sources. If not
+ specified, no function will be exported.
extra_cflags
The extra compiler flags for C++ compilation.
The default flags are:
@@ -604,7 +626,6 @@ def build_inline(
else:
cpp_source = _decorate_with_tvm_ffi(cpp_source, {})
cuda_source = _decorate_with_tvm_ffi(cuda_source, function_map)
-
# determine the cache dir for the built module
build_dir: Path
if build_directory is None:
@@ -694,11 +715,12 @@ def load_inline(
The CUDA source code. It can be a list of sources or a single source.
functions: Mapping[str, str] | Sequence[str] | str, optional
The functions in cpp_sources or cuda_source that will be exported to
the tvm ffi module. When a mapping is
- given, the keys are the names of the exported functions, and the
values are docstrings for the functions. When
- a sequence or a single string is given, they are the functions needed
to be exported, and the docstrings are set
- to empty strings. A single function name can also be given as a
string. When cpp_sources is given, the functions
- must be declared (not necessarily defined) in the cpp_sources. When
cpp_sources is not given, the functions
- must be defined in the cuda_sources. If not specified, no function
will be exported.
+ given, the keys are the names of the exported functions, and the
values are docstrings for the functions
+ (use an empty string to skip documentation for specific functions).
When a sequence or a single string is given, they are
+ the functions needed to be exported, and the docstrings are set to
empty strings. A single function name can
+ also be given as a string. When cpp_sources is given, the functions
must be declared (not necessarily defined)
+ in the cpp_sources. When cpp_sources is not given, the functions must
be defined in the cuda_sources. If not
+ specified, no function will be exported.
extra_cflags: Sequence[str], optional
The extra compiler flags for C++ compilation.
The default flags are:
diff --git a/python/tvm_ffi/module.py b/python/tvm_ffi/module.py
index 080d431..7b548d6 100644
--- a/python/tvm_ffi/module.py
+++ b/python/tvm_ffi/module.py
@@ -26,6 +26,7 @@ if TYPE_CHECKING:
# isort: on
# fmt: on
# tvm-ffi-stubgen(end)
+import json
from enum import IntEnum
from os import PathLike, fspath
from typing import ClassVar, cast
@@ -54,14 +55,23 @@ class Module(core.Object):
import tvm_ffi
- # load the module from a tvm-ffi shared library
- mod : tvm_ffi.Module = tvm_ffi.load_module("path/to/library.so")
- # you can use mod.func_name to call the exported function
+ # Load the module from a shared library
+ mod = tvm_ffi.load_module("path/to/library.so")
+
+ # Call exported function
mod.func_name(*args)
+ # Query function metadata (type signature)
+ metadata = mod.get_function_metadata("func_name")
+
+ # Query function documentation (if available)
+ doc = mod.get_function_doc("func_name")
+
See Also
--------
:py:func:`tvm_ffi.load_module`
+ :py:meth:`get_function_metadata`
+ :py:meth:`get_function_doc`
Notes
-----
@@ -175,6 +185,91 @@ class Module(core.Object):
raise AttributeError(f"Module has no function '{name}'")
return func
+ def get_function_metadata(
+ self, name: str, query_imports: bool = False
+ ) -> dict[str, Any] | None:
+ """Get metadata for a function exported from the module.
+
+ This retrieves metadata for functions exported via
c:macro:`TVM_FFI_DLL_EXPORT_TYPED_FUNC`
+ and when c:macro:`TVM_FFI_DLL_EXPORT_INCLUDE_METADATA` is on, which
includes type schema
+ information.
+
+ Parameters
+ ----------
+ name
+ The name of the function
+
+ query_imports
+ Whether to also query modules imported by this module.
+
+ Returns
+ -------
+ metadata
+ A dictionary containing function metadata. The ``type_schema``
field
+ encodes the callable signature.
+
+ Examples
+ --------
+ .. code-block:: python
+
+ import tvm_ffi
+ from tvm_ffi.core import TypeSchema
+ import json
+
+ mod = tvm_ffi.load_module("add_one_cpu.so")
+ metadata = mod.get_function_metadata("add_one_cpu")
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ print(schema) # Shows function signature
+
+ See Also
+ --------
+ :py:func:`tvm_ffi.get_global_func_metadata`
+ Get metadata for global registry functions.
+
+ """
+ metadata_str = _ffi_api.ModuleGetFunctionMetadata(self, name,
query_imports)
+ if metadata_str is None:
+ return None
+ return json.loads(metadata_str)
+
+ def get_function_doc(self, name: str, query_imports: bool = False) -> str
| None:
+ """Get documentation string for a function exported from the module.
+
+ This retrieves documentation for functions exported via
c:macro:`TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC`.
+
+ Parameters
+ ----------
+ name
+ The name of the function
+
+ query_imports
+ Whether to also query modules imported by this module.
+
+ Returns
+ -------
+ doc : str or None
+ The documentation string if available, None otherwise.
+
+ Examples
+ --------
+ .. code-block:: python
+
+ import tvm_ffi
+
+ mod = tvm_ffi.load_module("mylib.so")
+ doc = mod.get_function_doc("process_batch")
+ if doc:
+ print(doc)
+
+ See Also
+ --------
+ :py:meth:`get_function_metadata`
+ Get metadata including type schema.
+
+ """
+ doc_str = _ffi_api.ModuleGetFunctionDoc(self, name, query_imports)
+ return doc_str if doc_str else None
+
def import_module(self, module: Module) -> None:
"""Add module to the import list of current one.
diff --git a/src/ffi/extra/library_module.cc b/src/ffi/extra/library_module.cc
index f707c72..cc992aa 100644
--- a/src/ffi/extra/library_module.cc
+++ b/src/ffi/extra/library_module.cc
@@ -58,6 +58,30 @@ class LibraryModuleObj final : public ModuleObj {
return std::nullopt;
}
+ Optional<String> GetFunctionMetadata(const String& name) final {
+ // Look for __tvm_ffi__metadata_<name> symbol
+ String metadata_symbol = symbol::tvm_ffi_metadata_prefix + name;
+ void* symbol = lib_->GetSymbol(metadata_symbol);
+ if (symbol != nullptr) {
+ using MetadataGetter = int (*)(void*, const TVMFFIAny*, int32_t,
TVMFFIAny*);
+ auto metadata_getter = reinterpret_cast<MetadataGetter>(symbol);
+ return Function::InvokeExternC(nullptr, metadata_getter).cast<String>();
+ }
+ return std::nullopt;
+ }
+
+ Optional<String> GetFunctionDoc(const String& name) final {
+ // Look for __tvm_ffi__doc_<name> symbol
+ String doc_symbol = symbol::tvm_ffi_doc_prefix + name;
+ void* symbol = lib_->GetSymbol(doc_symbol);
+ if (symbol != nullptr) {
+ using DocGetter = int (*)(void*, const TVMFFIAny*, int32_t, TVMFFIAny*);
+ auto doc_getter = reinterpret_cast<DocGetter>(symbol);
+ return Function::InvokeExternC(nullptr, doc_getter).cast<String>();
+ }
+ return std::nullopt;
+ }
+
private:
ObjectPtr<Library> lib_;
};
diff --git a/src/ffi/testing/testing.cc b/src/ffi/testing/testing.cc
index 8d068b4..876801f 100644
--- a/src/ffi/testing/testing.cc
+++ b/src/ffi/testing/testing.cc
@@ -17,6 +17,8 @@
* under the License.
*/
// This file is used for testing the FFI API.
+#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 1
+
#include <dlpack/dlpack.h>
#include <tvm/ffi/any.h>
#include <tvm/ffi/container/array.h>
@@ -284,6 +286,82 @@ TVM_FFI_STATIC_INIT_BLOCK() {
namespace tvm {
namespace ffi {
+//
-----------------------------------------------------------------------------
+// Implementation functions for schema testing
+//
-----------------------------------------------------------------------------
+namespace schema_test_impl {
+
+// Simple types
+int64_t schema_id_int(int64_t x) { return x; }
+double schema_id_float(double x) { return x; }
+bool schema_id_bool(bool x) { return x; }
+DLDevice schema_id_device(DLDevice d) { return d; }
+DLDataType schema_id_dtype(DLDataType dt) { return dt; }
+String schema_id_string(String s) { return s; }
+Bytes schema_id_bytes(Bytes b) { return b; }
+Function schema_id_func(Function f) { return f; }
+TypedFunction<void(int64_t, float, Function)> schema_id_func_typed(
+ TypedFunction<void(int64_t, float, Function)> f) {
+ return f;
+}
+Any schema_id_any(Any a) { return a; }
+ObjectRef schema_id_object(ObjectRef o) { return o; }
+DLTensor* schema_id_dltensor(DLTensor* t) { return t; }
+Tensor schema_id_tensor(Tensor t) { return t; }
+void schema_tensor_view_input(TensorView t) {}
+
+// Optional types
+Optional<int64_t> schema_id_opt_int(Optional<int64_t> o) { return o; }
+Optional<String> schema_id_opt_str(Optional<String> o) { return o; }
+Optional<ObjectRef> schema_id_opt_obj(Optional<ObjectRef> o) { return o; }
+
+// Array types
+Array<int64_t> schema_id_arr_int(Array<int64_t> arr) { return arr; }
+Array<String> schema_id_arr_str(Array<String> arr) { return arr; }
+Array<ObjectRef> schema_id_arr_obj(Array<ObjectRef> arr) { return arr; }
+const ArrayObj* schema_id_arr(const ArrayObj* arr) { return arr; }
+
+// Map types
+Map<String, int64_t> schema_id_map_str_int(Map<String, int64_t> m) { return m;
}
+Map<String, String> schema_id_map_str_str(Map<String, String> m) { return m; }
+Map<String, ObjectRef> schema_id_map_str_obj(Map<String, ObjectRef> m) {
return m; }
+const MapObj* schema_id_map(const MapObj* m) { return m; }
+
+// Variant types
+Variant<int64_t, String> schema_id_variant_int_str(Variant<int64_t, String> v)
{ return v; }
+Variant<int64_t, String, Array<int64_t>> schema_variant_mix(
+ Variant<int64_t, String, Array<int64_t>> v) {
+ return v;
+}
+
+// Complex nested types
+Map<String, Array<int64_t>> schema_arr_map_opt(Array<Optional<int64_t>> arr,
+ Map<String, Array<int64_t>> mp,
+ Optional<String> os) {
+ // no-op combine
+ if (os.has_value()) {
+ Array<int64_t> extra;
+ for (const auto& i : arr) {
+ if (i.has_value()) extra.push_back(i.value());
+ }
+ mp.Set(os.value(), extra);
+ }
+ return mp;
+}
+
+// Edge cases
+int64_t schema_no_args() { return 1; }
+void schema_no_return(int64_t x) {}
+void schema_no_args_no_return() {}
+
+// Member function pattern
+int64_t test_int_pair_sum_wrapper(const TestIntPair& target) { return
target.Sum(); }
+
+// Documentation export
+int64_t test_add_with_docstring(int64_t a, int64_t b) { return a + b; }
+
+} // namespace schema_test_impl
+
// A class with a wide variety of field types and method signatures
class SchemaAllTypesObj : public Object {
public:
@@ -394,60 +472,42 @@ TVM_FFI_STATIC_INIT_BLOCK() {
// Global typed functions to exercise RegisterFunc with various schemas
refl::GlobalDef()
- .def(
- "testing.schema_id_int", [](int64_t x) { return x; },
- refl::Metadata{{"bool_attr", true}, //
- {"int_attr", 1}, //
- {"str_attr", "hello"}})
- .def("testing.schema_id_float", [](double x) { return x; })
- .def("testing.schema_id_bool", [](bool x) { return x; })
- .def("testing.schema_id_device", [](DLDevice d) { return d; })
- .def("testing.schema_id_dtype", [](DLDataType dt) { return dt; })
- .def("testing.schema_id_string", [](String s) { return s; })
- .def("testing.schema_id_bytes", [](Bytes b) { return b; })
- .def("testing.schema_id_func", [](Function f) -> Function { return f; })
- .def("testing.schema_id_func_typed",
- [](TypedFunction<void(int64_t, float, Function)> f)
- -> TypedFunction<void(int64_t, float, Function)> { return f; })
- .def("testing.schema_id_any", [](Any a) { return a; })
- .def("testing.schema_id_object", [](ObjectRef o) { return o; })
- .def("testing.schema_id_dltensor", [](DLTensor* t) { return t; })
- .def("testing.schema_id_tensor", [](Tensor t) { return t; })
- .def("testing.schema_tensor_view_input", [](TensorView t) -> void {})
- .def("testing.schema_id_opt_int", [](Optional<int64_t> o) { return o; })
- .def("testing.schema_id_opt_str", [](Optional<String> o) { return o; })
- .def("testing.schema_id_opt_obj", [](Optional<ObjectRef> o) { return o;
})
- .def("testing.schema_id_arr_int", [](Array<int64_t> arr) { return arr; })
- .def("testing.schema_id_arr_str", [](Array<String> arr) { return arr; })
- .def("testing.schema_id_arr_obj", [](Array<ObjectRef> arr) { return arr;
})
- .def("testing.schema_id_arr", [](const ArrayObj* arr) { return arr; })
- .def("testing.schema_id_map_str_int", [](Map<String, int64_t> m) {
return m; })
- .def("testing.schema_id_map_str_str", [](Map<String, String> m) { return
m; })
- .def("testing.schema_id_map_str_obj", [](Map<String, ObjectRef> m) {
return m; })
- .def("testing.schema_id_map", [](const MapObj* m) { return m; })
- .def("testing.schema_id_variant_int_str", [](Variant<int64_t, String> v)
{ return v; })
+ .def("testing.schema_id_int", schema_test_impl::schema_id_int,
+ refl::Metadata{{"bool_attr", true}, //
+ {"int_attr", 1}, //
+ {"str_attr", "hello"}})
+ .def("testing.schema_id_float", schema_test_impl::schema_id_float)
+ .def("testing.schema_id_bool", schema_test_impl::schema_id_bool)
+ .def("testing.schema_id_device", schema_test_impl::schema_id_device)
+ .def("testing.schema_id_dtype", schema_test_impl::schema_id_dtype)
+ .def("testing.schema_id_string", schema_test_impl::schema_id_string)
+ .def("testing.schema_id_bytes", schema_test_impl::schema_id_bytes)
+ .def("testing.schema_id_func", schema_test_impl::schema_id_func)
+ .def("testing.schema_id_func_typed",
schema_test_impl::schema_id_func_typed)
+ .def("testing.schema_id_any", schema_test_impl::schema_id_any)
+ .def("testing.schema_id_object", schema_test_impl::schema_id_object)
+ .def("testing.schema_id_dltensor", schema_test_impl::schema_id_dltensor)
+ .def("testing.schema_id_tensor", schema_test_impl::schema_id_tensor)
+ .def("testing.schema_tensor_view_input",
schema_test_impl::schema_tensor_view_input)
+ .def("testing.schema_id_opt_int", schema_test_impl::schema_id_opt_int)
+ .def("testing.schema_id_opt_str", schema_test_impl::schema_id_opt_str)
+ .def("testing.schema_id_opt_obj", schema_test_impl::schema_id_opt_obj)
+ .def("testing.schema_id_arr_int", schema_test_impl::schema_id_arr_int)
+ .def("testing.schema_id_arr_str", schema_test_impl::schema_id_arr_str)
+ .def("testing.schema_id_arr_obj", schema_test_impl::schema_id_arr_obj)
+ .def("testing.schema_id_arr", schema_test_impl::schema_id_arr)
+ .def("testing.schema_id_map_str_int",
schema_test_impl::schema_id_map_str_int)
+ .def("testing.schema_id_map_str_str",
schema_test_impl::schema_id_map_str_str)
+ .def("testing.schema_id_map_str_obj",
schema_test_impl::schema_id_map_str_obj)
+ .def("testing.schema_id_map", schema_test_impl::schema_id_map)
+ .def("testing.schema_id_variant_int_str",
schema_test_impl::schema_id_variant_int_str)
.def_packed("testing.schema_packed", [](PackedArgs args, Any* ret) {})
- .def("testing.schema_arr_map_opt",
- // NOLINTNEXTLINE(performance-unnecessary-value-param)
- [](Array<Optional<int64_t>> arr, Map<String, Array<int64_t>> mp,
- // NOLINTNEXTLINE(performance-unnecessary-value-param)
- Optional<String> os) -> Map<String, Array<int64_t>> {
- // no-op combine
- if (os.has_value()) {
- Array<int64_t> extra;
- for (const auto& i : arr) {
- if (i.has_value()) extra.push_back(i.value());
- }
- mp.Set(os.value(), extra);
- }
- return mp;
- })
- .def(
- "testing.schema_variant_mix",
- [](Variant<int64_t, String, Array<int64_t>> v) { return v; },
"variant passthrough")
- .def("testing.schema_no_args", []() { return 1; })
- .def("testing.schema_no_return", [](int64_t x) {})
- .def("testing.schema_no_args_no_return", []() {});
+ .def("testing.schema_arr_map_opt", schema_test_impl::schema_arr_map_opt)
+ .def("testing.schema_variant_mix", schema_test_impl::schema_variant_mix,
+ "variant passthrough")
+ .def("testing.schema_no_args", schema_test_impl::schema_no_args)
+ .def("testing.schema_no_return", schema_test_impl::schema_no_return)
+ .def("testing.schema_no_args_no_return",
schema_test_impl::schema_no_args_no_return);
TVMFFIEnvModRegisterSystemLibSymbol("__tvm_ffi_testing.add_one",
reinterpret_cast<void*>(__add_one_c_symbol));
}
@@ -455,4 +515,28 @@ TVM_FFI_STATIC_INIT_BLOCK() {
} // namespace ffi
} // namespace tvm
+//
-----------------------------------------------------------------------------
+// Exported symbols for metadata testing on DLL-exported functions
+//
-----------------------------------------------------------------------------
+// We keep minimal DLL exports here to verify the export mechanism.
+TVM_FFI_DLL_EXPORT_TYPED_FUNC(testing_dll_schema_id_int,
tvm::ffi::schema_test_impl::schema_id_int);
+
+// Documentation export
+TVM_FFI_DLL_EXPORT_TYPED_FUNC(testing_dll_test_add_with_docstring,
+
tvm::ffi::schema_test_impl::test_add_with_docstring);
+TVM_FFI_DLL_EXPORT_TYPED_FUNC_DOC(testing_dll_test_add_with_docstring,
+ R"(Add two integers and return the sum.
+
+Parameters
+----------
+a : int
+ First integer
+b : int
+ Second integer
+
+Returns
+-------
+result : int
+ Sum of a and b)");
+
extern "C" TVM_FFI_DLL_EXPORT int TVMFFITestingDummyTarget() { return 0; }
diff --git a/tests/cpp/test_function.cc b/tests/cpp/test_function.cc
index c274e51..b3879d7 100644
--- a/tests/cpp/test_function.cc
+++ b/tests/cpp/test_function.cc
@@ -16,9 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
+#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 1
+
#include <gtest/gtest.h>
#include <tvm/ffi/any.h>
#include <tvm/ffi/container/array.h>
+#include <tvm/ffi/container/map.h>
+#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/function.h>
#include <tvm/ffi/memory.h>
@@ -285,6 +289,17 @@ int invoke_testing_add1(int x) {
TEST(Func, InvokeExternC) { EXPECT_EQ(invoke_testing_add1(1), 2); }
+extern "C" int __tvm_ffi__metadata_testing_add1(void*, const TVMFFIAny*,
int32_t, TVMFFIAny*);
+
+TEST(Func, StaticLinkingMetadata) {
+ String metadata_str =
+ Function::InvokeExternC(nullptr,
__tvm_ffi__metadata_testing_add1).cast<String>();
+ Map<String, Any> metadata = json::Parse(metadata_str).cast<Map<String,
Any>>();
+ EXPECT_TRUE(metadata.count("type_schema"));
+ std::string type_schema_str = metadata["type_schema"].cast<String>();
+ EXPECT_TRUE(type_schema_str.find("int") != std::string::npos);
+}
+
extern "C" TVM_FFI_DLL int TVMFFITestingDummyTarget();
TEST(Func, DummyCFunc) {
diff --git a/tests/cpp/test_metadata.cc b/tests/cpp/test_metadata.cc
index 982893f..d0dfebd 100644
--- a/tests/cpp/test_metadata.cc
+++ b/tests/cpp/test_metadata.cc
@@ -29,11 +29,25 @@
#include <tvm/ffi/reflection/accessor.h>
#include <tvm/ffi/string.h>
+// Forward declarations for exported FFI functions
+extern "C" {
+int __tvm_ffi__metadata_testing_dll_schema_id_int(void*, const TVMFFIAny*,
int32_t, TVMFFIAny*);
+int __tvm_ffi__metadata_testing_dll_test_add_with_docstring(void*, const
TVMFFIAny*, int32_t,
+ TVMFFIAny*);
+int __tvm_ffi__doc_testing_dll_test_add_with_docstring(void*, const
TVMFFIAny*, int32_t,
+ TVMFFIAny*);
+}
+
namespace {
using namespace tvm::ffi;
using namespace tvm::ffi::reflection;
+// Helper to call metadata FFI functions and return the String result
+static String CallMetadataFunc(int (*func)(void*, const TVMFFIAny*, int32_t,
TVMFFIAny*)) {
+ return Function::InvokeExternC(nullptr, func).cast<String>();
+}
+
static std::string ParseMetadataToSchema(const String& metadata) {
return json::Parse(metadata)
.cast<Map<String, Any>>()["type_schema"] //
@@ -200,4 +214,32 @@ TEST(Schema, MethodTypeSchemas) {
R"({"type":"ffi.Function","args":[{"type":"testing.SchemaAllTypes"},{"type":"int"},{"type":"float"},{"type":"ffi.String"}]})");
}
+TEST(Schema, DLLExportedFuncMetadata) {
+ // Minimal sanity check that DLL export metadata mechanism works.
+
EXPECT_EQ(ParseMetadataToSchema(CallMetadataFunc(__tvm_ffi__metadata_testing_dll_schema_id_int)),
+
R"({"type":"ffi.Function","args":[{"type":"int"},{"type":"int"}]})");
+}
+
+TEST(Schema, DLLExportedFuncDocumentation) {
+ EXPECT_EQ(ParseMetadataToSchema(
+
CallMetadataFunc(__tvm_ffi__metadata_testing_dll_test_add_with_docstring)),
+
R"({"type":"ffi.Function","args":[{"type":"int"},{"type":"int"},{"type":"int"}]})");
+ String doc =
CallMetadataFunc(__tvm_ffi__doc_testing_dll_test_add_with_docstring);
+ std::string doc_str(doc);
+ EXPECT_EQ(doc_str,
+ R"(Add two integers and return the sum.
+
+Parameters
+----------
+a : int
+ First integer
+b : int
+ Second integer
+
+Returns
+-------
+result : int
+ Sum of a and b)");
+}
+
} // namespace
diff --git a/tests/python/test_build.cc b/tests/python/test_build.cc
index e25e8cf..bc6acf1 100644
--- a/tests/python/test_build.cc
+++ b/tests/python/test_build.cc
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
+#define TVM_FFI_DLL_EXPORT_INCLUDE_METADATA 1
+
#include "test_build.h"
#include <tvm/ffi/container/tensor.h>
diff --git a/tests/python/test_build.py b/tests/python/test_build.py
index 30153dc..7f22669 100644
--- a/tests/python/test_build.py
+++ b/tests/python/test_build.py
@@ -14,11 +14,14 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
+import gc
import pathlib
import numpy
import pytest
+import tvm_ffi
import tvm_ffi.cpp
+from tvm_ffi.core import TypeSchema
from tvm_ffi.module import Module
@@ -31,11 +34,284 @@ def test_build_cpp() -> None:
mod: Module = tvm_ffi.load_module(output_lib_path)
+ metadata = mod.get_function_metadata("add_one_cpu")
+ assert metadata is not None, "add_one_cpu should have metadata"
+ assert "type_schema" in metadata, f"{'add_one_cpu'}: {metadata}"
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[Tensor, Tensor], None]",
f"{'add_one_cpu'}: {schema}"
+ doc = mod.get_function_doc("add_one_cpu")
+ assert doc is None
+
x = numpy.array([1, 2, 3, 4, 5], dtype=numpy.float32)
y = numpy.empty_like(x)
mod.add_one_cpu(x, y)
numpy.testing.assert_equal(x + 1, y)
+def test_build_inline_with_metadata() -> None: # noqa: PLR0915
+ """Test functions with various input and output types."""
+ # Keep module alive until all returned objects are destroyed
+ mod: Module = tvm_ffi.cpp.load_inline(
+ name="test_io_types",
+ cpp_sources=r"""
+ // int input -> int output
+ int square(int x) {
+ return x * x;
+ }
+
+ // float input -> float output
+ float reciprocal(float x) {
+ return 1.0f / x;
+ }
+
+ // bool input -> bool output
+ bool negate(bool x) {
+ return !x;
+ }
+
+ // String input -> String output
+ tvm::ffi::String uppercase_first(tvm::ffi::String s) {
+ std::string result(s.c_str());
+ if (!result.empty()) {
+ result[0] = std::toupper(result[0]);
+ }
+ return tvm::ffi::String(result);
+ }
+
+ // Multiple inputs: int, float -> float
+ float weighted_sum(int count, float weight) {
+ return static_cast<float>(count) * weight;
+ }
+
+ // Multiple inputs: String, int -> String
+ tvm::ffi::String repeat_string(tvm::ffi::String s, int times) {
+ std::string result;
+ for (int i = 0; i < times; ++i) {
+ result += s.c_str();
+ }
+ return tvm::ffi::String(result);
+ }
+
+ // Mixed types: bool, int, float, String -> String
+ tvm::ffi::String format_data(bool flag, int count, float value,
tvm::ffi::String label) {
+ std::ostringstream oss;
+ oss << label.c_str() << ": flag=" << (flag ? "true" : "false")
+ << ", count=" << count << ", value=" << value;
+ return tvm::ffi::String(oss.str());
+ }
+
+ // Tensor input/output
+ void double_tensor(tvm::ffi::TensorView input,
tvm::ffi::TensorView output) {
+ TVM_FFI_ICHECK(input.ndim() == 1);
+ TVM_FFI_ICHECK(output.ndim() == 1);
+ TVM_FFI_ICHECK(input.size(0) == output.size(0));
+ DLDataType f32_dtype{kDLFloat, 32, 1};
+ TVM_FFI_ICHECK(input.dtype() == f32_dtype);
+ TVM_FFI_ICHECK(output.dtype() == f32_dtype);
+
+ for (int i = 0; i < input.size(0); ++i) {
+ static_cast<float*>(output.data_ptr())[i] =
+ static_cast<const float*>(input.data_ptr())[i] * 2.0f;
+ }
+ }
+ """,
+ functions=[
+ "square",
+ "reciprocal",
+ "negate",
+ "uppercase_first",
+ "weighted_sum",
+ "repeat_string",
+ "format_data",
+ "double_tensor",
+ ],
+ extra_cflags=["-DTVM_FFI_DLL_EXPORT_INCLUDE_METADATA=1"],
+ )
+
+ # Test square: int -> int
+ assert mod.square(5) == 25
+ metadata = mod.get_function_metadata("square")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[int], int]"
+
+ # Test reciprocal: float -> float
+ result = mod.reciprocal(2.0)
+ assert abs(result - 0.5) < 0.001
+ metadata = mod.get_function_metadata("reciprocal")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[float], float]"
+
+ # Test negate: bool -> bool
+ assert mod.negate(True) is False
+ assert mod.negate(False) is True
+ metadata = mod.get_function_metadata("negate")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[bool], bool]"
+
+ # Test uppercase_first: String -> String
+ result = mod.uppercase_first("hello")
+ assert result == "Hello"
+ metadata = mod.get_function_metadata("uppercase_first")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[str], str]"
+
+ # Test weighted_sum: int, float -> float
+ result = mod.weighted_sum(10, 2.5)
+ assert abs(result - 25.0) < 0.001
+ metadata = mod.get_function_metadata("weighted_sum")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[int, float], float]"
+
+ # Test repeat_string: String, int -> String
+ result = mod.repeat_string("ab", 3)
+ assert result == "ababab"
+ metadata = mod.get_function_metadata("repeat_string")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[str, int], str]"
+
+ # Test format_data: bool, int, float, String -> String
+ result = mod.format_data(True, 42, 3.14, "test")
+ assert "test:" in result
+ assert "flag=true" in result
+ assert "count=42" in result
+ assert "value=3.14" in result
+ metadata = mod.get_function_metadata("format_data")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[bool, int, float, str], str]"
+
+ # Test double_tensor: Tensor, Tensor -> None
+ x = numpy.array([1.0, 2.0, 3.0], dtype=numpy.float32)
+ y = numpy.empty_like(x)
+ mod.double_tensor(x, y)
+ numpy.testing.assert_allclose(y, x * 2.0)
+ metadata = mod.get_function_metadata("double_tensor")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[Tensor, Tensor], None]"
+
+ # Explicitly cleanup all objects before module unload to avoid
use-after-free
+ del metadata, schema, result, x, y, mod
+ gc.collect()
+
+
+def test_build_inline_with_docstrings() -> None:
+ """Test building functions with documentation using the functions dict."""
+ # Keep module alive until all returned objects are destroyed
+ add_docstring = (
+ "Add two integers and return the sum.\n"
+ "\n"
+ "Parameters\n"
+ "----------\n"
+ "a : int\n"
+ " First integer\n"
+ "b : int\n"
+ " Second integer\n"
+ "\n"
+ "Returns\n"
+ "-------\n"
+ "result : int\n"
+ " Sum of a and b"
+ )
+
+ divide_docstring = "Divides two floats. Returns a/b."
+
+ mod: Module = tvm_ffi.cpp.load_inline(
+ name="test_docs",
+ cpp_sources=r"""
+ int add(int a, int b) {
+ return a + b;
+ }
+
+ int subtract(int a, int b) {
+ return a - b;
+ }
+
+ float divide(float a, float b) {
+ TVM_FFI_ICHECK(b != 0.0f) << "Division by zero";
+ return a / b;
+ }
+ """,
+ functions={
+ "add": add_docstring,
+ "subtract": "", # No documentation
+ "divide": divide_docstring,
+ },
+ extra_cflags=["-DTVM_FFI_DLL_EXPORT_INCLUDE_METADATA=1"],
+ )
+
+ # Test add function with full documentation
+ assert mod.add(10, 5) == 15
+ metadata = mod.get_function_metadata("add")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[int, int], int]"
+
+ doc = mod.get_function_doc("add")
+ assert doc is not None, "add should have documentation"
+ assert doc == add_docstring
+
+ # Test subtract function without documentation
+ assert mod.subtract(10, 5) == 5
+ metadata = mod.get_function_metadata("subtract")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[int, int], int]"
+
+ doc = mod.get_function_doc("subtract")
+ assert doc is None, "subtract should not have documentation"
+
+ # Test divide function with short documentation
+ result = mod.divide(10.0, 2.0)
+ assert abs(result - 5.0) < 0.001
+ metadata = mod.get_function_metadata("divide")
+ assert metadata is not None
+ schema = TypeSchema.from_json_str(metadata["type_schema"])
+ assert str(schema) == "Callable[[float, float], float]"
+
+ doc = mod.get_function_doc("divide")
+ assert doc is not None, "divide should have documentation"
+ assert doc == divide_docstring
+
+ # Explicitly cleanup all objects before module unload to avoid
use-after-free
+ del metadata, schema, doc, result, mod
+ gc.collect()
+
+
+def test_build_without_metadata() -> None:
+ """Test building without metadata export."""
+ mod: Module = tvm_ffi.cpp.load_inline(
+ name="test_no_meta",
+ cpp_sources=r"""
+ // Note: NOT defining TVM_FFI_DLL_EXPORT_INCLUDE_METADATA
+
+ int simple_add(int a, int b) {
+ return a + b;
+ }
+ """,
+ functions=["simple_add"],
+ )
+
+ # Function should still work
+ result = mod.simple_add(10, 20)
+ assert result == 30
+
+ # But metadata should not be available
+ metadata = mod.get_function_metadata("simple_add")
+ assert metadata is None, (
+ "Metadata should not be available without
TVM_FFI_DLL_EXPORT_INCLUDE_METADATA"
+ )
+
+ # Doc should also not be available
+ doc = mod.get_function_doc("simple_add")
+ assert doc is None
+
+
if __name__ == "__main__":
pytest.main([__file__])