Skip to content

Commit c78e8b4

Browse files
authored
doc: abi overview (#402)
1 parent e1bd421 commit c78e8b4

File tree

10 files changed

+977
-876
lines changed

10 files changed

+977
-876
lines changed

docs/concepts/abi_overview.md

Lines changed: 0 additions & 462 deletions
This file was deleted.

docs/concepts/abi_overview.rst

Lines changed: 551 additions & 0 deletions
Large diffs are not rendered by default.

docs/concepts/any.rst

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ values of a wide variety of types, including primitives, objects, and strings.
2727
Unlike ``std::any``, it is designed for zero-copy inter-language exchange without RTTI,
2828
featuring a fixed 16-byte layout with built-in reference counting and ownership semantics.
2929

30-
This tutorial covers everything you need to know about :cpp:class:`~tvm::ffi::Any` and :cpp:class:`~tvm::ffi::AnyView`:
31-
common usage patterns, ownership semantics, and memory layout.
32-
30+
This tutorial covers common usage patterns, ownership semantics, and memory layout.
3331

3432
Common Usage
3533
------------
@@ -169,6 +167,8 @@ Compare with ``nullptr`` to check for ``None``:
169167
}
170168
171169
170+
.. _any-ownership:
171+
172172
Ownership
173173
---------
174174

@@ -195,8 +195,8 @@ The core distinction between :cpp:class:`tvm::ffi::Any` and
195195
- Function inputs
196196
- Return values, storage
197197

198-
Code Examples
199-
~~~~~~~~~~~~~~
198+
Examples
199+
~~~~~~~~
200200

201201
:cpp:class:`~tvm::ffi::AnyView` is a lightweight, non-owning view. Copying it simply
202202
copies 16 bytes with no reference count updates, making it ideal for passing arguments without overhead:
@@ -245,25 +245,9 @@ Destruction Semantics in C
245245

246246
In C, which lacks RAII, you must manually destroy :cpp:class:`~tvm::ffi::Any` objects
247247
by calling :cpp:func:`TVMFFIObjectDecRef` for heap-allocated objects.
248+
Destroying an :cpp:class:`~tvm::ffi::AnyView` is effectively a no-op - just clear its contents.
248249

249-
.. code-block:: cpp
250-
251-
void destroy_any(TVMFFIAny* any) {
252-
if (any->type_index >= kTVMFFIStaticObjectBegin) {
253-
// Decrement the reference count of the heap-allocated object
254-
TVMFFIObjectDecRef(any->v_obj);
255-
}
256-
*any = (TVMFFIAny){0};
257-
}
258-
259-
In contrast, destroying an :cpp:class:`~tvm::ffi::AnyView` is effectively a no-op - just clear its contents.
260-
261-
.. code-block:: cpp
262-
263-
void destroy_any_view(TVMFFIAny* any_view) {
264-
*any_view = (TVMFFIAny){0};
265-
}
266-
250+
See :ref:`abi-destruct-any` for C code examples.
267251

268252
Layout
269253
------
@@ -310,6 +294,8 @@ It is effectively a layout-stable 16-byte tagged union.
310294
* The first 4 bytes (:cpp:member:`TVMFFIAny::type_index`) serve as a tag identifying the stored type.
311295
* The last 8 bytes hold the actual value - either stored inline for atomic types (e.g., ``int64_t``, ``float64``, ``void*``) or as a pointer to a heap-allocated object.
312296

297+
.. _any-atomic-types:
298+
313299
Atomic Types
314300
~~~~~~~~~~~~
315301

@@ -371,6 +357,8 @@ Note that raw pointers like :c:struct:`DLTensor* <DLTensor>` and ``char*`` also
371357
These pointers carry no ownership, so the caller must ensure the pointed-to data outlives
372358
the :cpp:class:`~tvm::ffi::AnyView` or :cpp:class:`~tvm::ffi::Any`.
373359

360+
.. _any-heap-allocated-objects:
361+
374362
Heap-Allocated Objects
375363
~~~~~~~~~~~~~~~~~~~~~~
376364

@@ -431,7 +419,7 @@ inline using **small string optimization**, avoiding heap allocation entirely:
431419
Further Reading
432420
---------------
433421

434-
- **Object system**: :doc:`object_and_class` covers how TVM-FFI objects work, including reference counting and type checking
435-
- **Function system**: :doc:`func_module` covers function calling conventions and the global registry
436-
- **C examples**: :doc:`../get_started/stable_c_abi` demonstrates working with :cpp:class:`TVMFFIAny` directly in C
437-
- **Tensor conversions**: :doc:`tensor` covers how tensors flow through :cpp:class:`~tvm::ffi::Any` and :cpp:class:`~tvm::ffi::AnyView`
422+
- :doc:`object_and_class`: How TVM-FFI objects work, including reference counting and type checking
423+
- :doc:`func_module`: Function calling conventions and the global registry
424+
- :doc:`tensor`: How tensors flow through :cpp:class:`~tvm::ffi::Any` and :cpp:class:`~tvm::ffi::AnyView`
425+
- :doc:`abi_overview`: Low-level C ABI details for working with :cpp:class:`TVMFFIAny` directly

docs/concepts/func_module.rst

Lines changed: 59 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@
1515
specific language governing permissions and limitations
1616
under the License.
1717
18-
Function, Exception and Module
19-
==============================
18+
Function and Module
19+
===================
2020

2121
TVM-FFI provides a unified and ABI-stable calling convention that enables
2222
cross-language function calls between C++, Python, Rust, and other languages.
2323
Functions are first-class :doc:`TVM-FFI objects <object_and_class>`.
2424

25-
This tutorial covers everything you need to know about defining, registering,
26-
and calling TVM-FFI functions, their exception handling, and working with modules.
25+
This tutorial covers defining, registering, and calling TVM-FFI functions,
26+
exception handling, and working with modules.
2727

2828
Glossary
2929
--------
3030

31-
TVM-FFI ABI. :cpp:type:`TVMFFISafeCallType`
31+
TVM-FFI ABI, or "Packed Function". :cpp:type:`TVMFFISafeCallType`
3232
A stable C calling convention where every function is represented by a single signature,
3333
which enables type-erased, cross-language function calls.
3434
This calling convention is used across all TVM-FFI function calls at the ABI boundary.
@@ -190,17 +190,15 @@ to a :py:class:`tvm_ffi.Function` at the ABI boundary. The example below demonst
190190
print(func_add(1, 2))
191191
192192
193-
Exception ABI
194-
-------------
193+
.. _sec:function:
195194

196-
This section describes the exception handling contract in the TVM-FFI Stable C ABI.
197-
Exceptions are first-class citizens in TVM-FFI, and this section specifies:
195+
Function
196+
--------
198197

199-
- How to properly throw exceptions from a TVM-FFI ABI function
200-
- How to check for and propagate exceptions from a TVM-FFI ABI function
198+
.. _sec:function-calling-convention:
201199

202-
TVM-FFI C ABI
203-
~~~~~~~~~~~~~
200+
Calling Convention
201+
~~~~~~~~~~~~~~~~~~
204202

205203
All TVM-FFI functions ultimately conform to the :cpp:type:`TVMFFISafeCallType` signature,
206204
which provides a stable C ABI for cross-language calls. The C calling convention is defined as:
@@ -220,147 +218,48 @@ specified by ``args`` and ``num_args``.
220218
**Output argument**. The output argument ``result`` is an owning :cpp:type:`tvm::ffi::Any`
221219
that the caller must zero-initialize before the call.
222220

221+
.. important::
222+
The caller must zero-initialize the output argument ``result`` before the call.
223+
223224
**Return value**. The ABI returns an **error code** that indicates:
224225

225-
- ``0``: Success
226-
- ``-1``: Error occurred, retrievable with :cpp:func:`TVMFFIErrorMoveFromRaised`
227-
- ``-2``: Very rare frontend error
226+
- **Error code 0**: Success
227+
- **Error code -1**: Error occurred, retrievable with :cpp:func:`TVMFFIErrorMoveFromRaised`
228+
- **Error code -2**: Very rare frontend error
228229

229230
.. hint::
230231
See :doc:`Any <any>` for more details on the semantics of :cpp:type:`tvm::ffi::AnyView` and :cpp:type:`tvm::ffi::Any`.
231232

232-
Retrieve Errors in C
233-
~~~~~~~~~~~~~~~~~~~~
234-
235-
When a TVM-FFI function returns a non-zero code, it indicates that an error occurred
236-
and a :cpp:class:`tvm::ffi::ErrorObj` is stored in thread-local storage (TLS).
237-
This section shows how to retrieve the error object and print the error message and backtrace.
238-
239-
.. note::
240-
241-
An :cpp:class:`~tvm::ffi::ErrorObj` is a :cpp:class:`~tvm::ffi::Object` with a :cpp:class:`TVMFFIErrorCell` payload
242-
as defined below:
243-
244-
.. code-block:: cpp
245-
246-
typedef struct {
247-
TVMFFIByteArray kind; // Error type (e.g., "ValueError")
248-
TVMFFIByteArray message; // Error message
249-
TVMFFIByteArray backtrace; // Stack trace (most-recent call first)
250-
void (*update_backtrace)(...); // Hook to append/replace backtrace
251-
} TVMFFIErrorCell;
252-
253-
**Print an Error**. The example code below shows how to print an error message and backtrace.
254-
255-
.. code-block:: cpp
256-
257-
#include <tvm/ffi/c_api.h>
258-
259-
void PrintError(TVMFFIObject* err) {
260-
TVMFFIErrorCell* cell = (TVMFFIErrorCell*)((char*)err + sizeof(TVMFFIObject));
261-
fprintf(stderr, "%.*s: %.*s\n", (int)cell->kind.size, cell->kind.data, (int)cell->message.size, cell->message.data);
262-
if (cell->backtrace.size) {
263-
fprintf(stderr, "Backtrace:\n%.*s\n", (int)cell->backtrace.size, cell->backtrace.data);
264-
}
265-
}
266-
267-
The payload of the error object is a :cpp:type:`TVMFFIErrorCell` structure
268-
containing the error kind, message, and backtrace. It can be accessed
269-
by skipping the :cpp:type:`TVMFFIObject` header using pointer arithmetic.
270-
271-
**Retrieve the error object**. When the error code is ``-1``, the error object is stored in TLS
272-
and can be retrieved with :cpp:func:`TVMFFIErrorMoveFromRaised`.
233+
This design is called a **packed function**, because it "packs" all arguments into a single array of type-erased :cpp:type:`tvm::ffi::AnyView`,
234+
and further unifies calling convention across all languages without resorting to JIT compilation.
273235

274-
.. code-block:: cpp
275-
276-
void HandleReturnCode(int rc) {
277-
TVMFFIObject* err = NULL;
278-
if (rc == 0) {
279-
// Success
280-
} else if (rc == -1) {
281-
// Move the raised error from TLS (clears TLS slot)
282-
TVMFFIErrorMoveFromRaised(&err); // now `err` owns the error object
283-
if (err != NULL) {
284-
PrintError(err); // print the error
285-
TVMFFIObjectDecRef(err); // Release the error object
286-
}
287-
} else if (rc == -2) {
288-
// Frontend (e.g., Python) already has an exception set.
289-
// Do not fetch from TLS; consult the frontend's error mechanism.
290-
}
291-
}
236+
More specifically, this mechanism enables the following scenarios:
292237

293-
This function transfers ownership of the error object to the caller and clears the TLS slot.
294-
You must call :cpp:func:`TVMFFIObjectDecRef` to release the object when done to avoid memory leaks.
238+
- **Dynamic languages**. Well-optimized bindings are provided for, e.g. Python, to translate arguments into packed function format, and translate return value back to the host language.
239+
- **Static languages**. Metaprogramming techniques, such as C++ templates, are usually available to directly instantiate packed format on stack, saving the need for dynamic examination.
240+
- **Cross-language callbacks**. Language-agnostic :cpp:class:`tvm::ffi::Function` makes it easy to call between languages without depending on language-specific features such as GIL.
295241

296-
**Rare frontend errors**. Error code ``-2`` is reserved for rare frontend errors. It is returned only
297-
when the C API :cpp:func:`TVMFFIEnvCheckSignals` returns non-zero during execution, indicating that
298-
the Python side has a pending signal requiring attention. In this case, the caller should not fetch
299-
the error object from TLS but instead consult the frontend's error mechanism to handle the exception.
242+
**Performance Implications**. This approach is in practice highly efficient in machine learning workloads.
300243

301-
Raise Errors in C
302-
~~~~~~~~~~~~~~~~~
303-
304-
As part of TVM-FFI's calling convention, returning ``-1`` indicates that an error occurred
305-
and the error object is stored in the TLS slot. The error object can contain arbitrary
306-
user-defined information, such as error messages, backtraces, or Python frame-local variables.
307-
308-
.. hint::
309-
Compiler code generation may use similar patterns to raise errors in generated code.
244+
- In Python/C++ calls, we can get to microsecond level overhead, which is generally similar to overhead for eager mode;
245+
- When both sides of calls are static languages, the overhead will go down to tens of nanoseconds.
310246

311-
The example below sets the TLS error and returns ``-1`` using :cpp:func:`TVMFFIErrorSetRaisedFromCStr`:
312-
313-
.. code-block:: cpp
314-
315-
#include <tvm/ffi/c_api.h>
316-
317-
int __tvm_ffi_my_kernel(void* handle, const TVMFFIAny* args,
318-
int32_t num_args, TVMFFIAny* result) {
319-
// Validate inputs
320-
if (num_args < 2) {
321-
TVMFFIErrorSetRaisedFromCStr("ValueError", "Expected at least 2 arguments");
322-
return -1;
323-
}
324-
// ... kernel implementation ...
325-
return 0;
326-
}
327-
328-
Alternatively, :cpp:func:`TVMFFIErrorSetRaisedFromCStrParts` accepts explicit string lengths,
329-
which is useful when the error kind and message are not null-terminated.
330-
331-
**Propagating errors**. For chains of generated calls, simply propagate return codes—TLS carries
332-
the error details:
333-
334-
.. code-block:: cpp
335-
336-
int outer_function(...) {
337-
int err_code = 0;
338-
339-
err_code = inner_function(...);
340-
if (err_code != 0) goto RAII; // Propagate error; TLS has the details
247+
.. note::
248+
Although we found it less necessary in practice, further link time optimization (LTO) is still theoretically possible
249+
in scenarios where both sides are static languages with a known symbol and linked into a single binary.
250+
In this case, the callee can be inlined into caller side and the stack argument memory can be passed into register passing.
341251

342-
RAII:
343-
// clean up owned resources
344-
return err_code;
345-
}
346-
347-
Function
348-
--------
252+
.. _sec:function-layout:
349253

350254
Layout and ABI
351255
~~~~~~~~~~~~~~
352256

353257
:cpp:class:`tvm::ffi::FunctionObj` stores two call pointers in :cpp:class:`TVMFFIFunctionCell`:
354258

355-
.. code-block:: cpp
356-
357-
typedef struct {
358-
TVMFFISafeCallType safe_call;
359-
void* cpp_call;
360-
} TVMFFIFunctionCell;
259+
- ``safe_call``: Used for cross-ABI function calls; intercepts exceptions and stores them in TLS.
260+
- ``cpp_call``: Used within the same DSO; exceptions are thrown directly for better performance.
361261

362-
``safe_call`` is used for cross-ABI function calls: it intercepts exceptions and stores them in TLS.
363-
``cpp_call`` is used within the same DSO, where exceptions are thrown directly for better performance.
262+
See :ref:`abi-function` for the C struct definition.
364263

365264
.. important::
366265

@@ -407,40 +306,35 @@ calling convention.
407306
in :c:macro:`TVM_FFI_SAFE_CALL_BEGIN` / :c:macro:`TVM_FFI_SAFE_CALL_END` macros.
408307

409308

410-
C Registry APIs
411-
~~~~~~~~~~~~~~~
309+
Compiler developers commonly need to look up global functions in generated code.
310+
Use :cpp:func:`TVMFFIFunctionGetGlobal` to retrieve a function by name, then call it with :cpp:func:`TVMFFIFunctionCall`.
311+
See :ref:`abi-function` for C code examples.
412312

413-
.. list-table::
414-
:header-rows: 1
415-
:widths: 40 60
313+
.. _sec:exception:
416314

417-
* - C API
418-
- Description
419-
* - :cpp:func:`TVMFFIFunctionGetGlobal`
420-
- Get a function by name; returns an owning handle.
421-
* - :cpp:func:`TVMFFIFunctionSetGlobal`
422-
- Register a function in the global registry.
423-
* - :cpp:func:`TVMFFIFunctionCall`
424-
- Call a function with the given arguments.
315+
Exception
316+
~~~~~~~~~
425317

426-
Compiler developers commonly need to look up global functions in generated code. Use
427-
:cpp:func:`TVMFFIFunctionGetGlobal` to retrieve a function by name, then call it with :cpp:func:`TVMFFIFunctionCall`.
428-
The example below demonstrates how to look up and call a global function in C:
318+
This section describes the exception handling contract in the TVM-FFI Stable C ABI.
319+
Exceptions are first-class citizens in TVM-FFI, and this section specifies:
429320

430-
.. code-block:: cpp
321+
- How to properly throw exceptions from a TVM-FFI ABI function
322+
- How to check for and propagate exceptions from a TVM-FFI ABI function
431323

432-
int LookupAndCall(const char* global_function_name, const TVMFFIAny* args, int num_args, TVMFFIAny* result) {
433-
TVMFFIObject* func = NULL;
434-
int err_code;
435-
if ((err_code = TVMFFIFunctionGetGlobal(global_function_name, &func)) != 0)
436-
goto RAII;
437-
if ((err_code = TVMFFIFunctionCall(func, args, num_args, result)) != 0)
438-
goto RAII;
439-
440-
RAII: // clean up owned resources
441-
if (func != NULL) TVMFFIObjectDecRef(func);
442-
return err_code;
443-
}
324+
When a TVM-FFI function returns a non-zero code, an error occurred.
325+
An :cpp:class:`~tvm::ffi::ErrorObj` is stored in thread-local storage (TLS) and can be retrieved
326+
with :cpp:func:`TVMFFIErrorMoveFromRaised`.
327+
328+
- **Error code -1:** Retrieve the error from TLS, print it, and release via :cpp:func:`TVMFFIObjectDecRef`.
329+
- **Error code -2:** A rare frontend error; consult the frontend's error mechanism instead of TLS.
330+
331+
To raise an error, use :cpp:func:`TVMFFIErrorSetRaisedFromCStr` to set the TLS error and return ``-1``.
332+
For chains of calls, simply propagate return codes - TLS carries the error details.
333+
334+
See :ref:`abi-exception` for C code examples.
335+
336+
337+
.. _sec:module:
444338

445339
Modules
446340
-------
@@ -560,5 +454,5 @@ Further Reading
560454

561455
- :doc:`any`: How functions are stored in :cpp:class:`~tvm::ffi::Any` containers
562456
- :doc:`object_and_class`: The object system that backs :cpp:class:`~tvm::ffi::FunctionObj`
457+
- :doc:`abi_overview`: Low-level C ABI details for functions and exceptions
563458
- :doc:`../packaging/python_packaging`: Packaging functions for Python wheels
564-
- :doc:`abi_overview`: Low-level ABI details for the function calling convention

0 commit comments

Comments
 (0)