Repository: apache/tvm

Issue:
C Interface API Generation split between Python and C++
Currently the Interface API header is generated in Python:
https://github.com/apache/tvm/blob/main/python/tvm/micro/interface_api.py

And the source it references is generated in C++:
https://github.com/apache/tvm/blob/main/src/target/source/source_module.cc#L237

This leads to re-implementing logic, such as name sanitising, in the two different languages. These should be consolidated to generating both header and source in C++. My opening suggestion for a file name is `src/target/source/interface_c.cc` 😸 

See discussion here: https://github.com/apache/tvm/pull/8720


Discussion:

Resolving patch:
diff --git a/python/tvm/micro/interface_api.py b/python/tvm/micro/interface_api.py
deleted file mode 100644
--- a/python/tvm/micro/interface_api.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-"""Defines functions for generating a C interface header"""
-
-# TODO: Currently the Interface API header is generated in Python but the source it references
-# is generated in C++. These should be consolidated to generate both header and source in C++
-# and avoid re-implementing logic, such as name sanitising, in the two different languages.
-# See https://github.com/apache/tvm/issues/8792 .
-
-import os
-import re
-
-from tvm.relay.backend.utils import mangle_module_name
-
-
-def _emit_brief(header_file, module_name, description):
-    header_file.write("/*!\n")
-    header_file.write(f' * \\brief {description} for TVM module "{module_name}" \n')
-    header_file.write(" */\n")
-
-
-def generate_c_interface_header(module_name, inputs, outputs, output_path):
-    """Generates a C interface header for a given modules inputs and outputs
-
-    Parameters
-    ----------
-    module_name : str
-        Name of the module to be used in defining structs and naming the header
-    inputs : list[str]
-        List of module input names to be placed in generated structs
-    outputs : list[str]
-        List of module output names to be placed in generated structs
-    output_path : str
-        Path to the output folder to generate the header into
-
-    Returns
-    -------
-    str :
-        Name of the generated file.
-    """
-    mangled_name = mangle_module_name(module_name)
-    metadata_header = os.path.join(output_path, f"{mangled_name}.h")
-    with open(metadata_header, "w") as header_file:
-        header_file.write(
-            f"#ifndef {mangled_name.upper()}_H_\n"
-            f"#define {mangled_name.upper()}_H_\n\n"
-            "#include <stdint.h>\n\n"
-            "#ifdef __cplusplus\n"
-            'extern "C" {\n'
-            "#endif\n\n"
-        )
-
-        _emit_brief(header_file, module_name, "Input tensor pointers")
-        header_file.write(f"struct {mangled_name}_inputs {{\n")
-        sanitized_names = []
-        for input_name in inputs:
-            sanitized_input_name = re.sub(r"\W", "_", input_name)
-            if sanitized_input_name in sanitized_names:
-                raise ValueError(f"Sanitized input tensor name clash: {sanitized_input_name}")
-            sanitized_names.append(sanitized_input_name)
-            header_file.write(f"  void* {sanitized_input_name};\n")
-        header_file.write("};\n\n")
-
-        _emit_brief(header_file, module_name, "Output tensor pointers")
-        header_file.write(f"struct {mangled_name}_outputs {{\n")
-        for output_name in outputs:
-            header_file.write(f"  void* {output_name};\n")
-        header_file.write("};\n\n")
-
-        header_file.write(
-            "/*!\n"
-            f' * \\brief entrypoint function for TVM module "{module_name}"\n'
-            " * \\param inputs Input tensors for the module \n"
-            " * \\param outputs Output tensors for the module \n"
-            " */\n"
-            f"int32_t {mangled_name}_run(\n"
-            f"  struct {mangled_name}_inputs* inputs,\n"
-            f"  struct {mangled_name}_outputs* outputs\n"
-            ");\n"
-        )
-
-        header_file.write(
-            "\n#ifdef __cplusplus\n}\n#endif\n\n" f"#endif // {mangled_name.upper()}_H_\n"
-        )
-
-    return metadata_header
diff --git a/python/tvm/micro/model_library_format.py b/python/tvm/micro/model_library_format.py
--- a/python/tvm/micro/model_library_format.py
+++ b/python/tvm/micro/model_library_format.py
@@ -25,13 +25,14 @@
 import tarfile
 import typing
 
+import tvm
 from tvm.ir.type import TupleType
 from .._ffi import get_global_func
-from .interface_api import generate_c_interface_header
 from ..contrib import utils
 from ..driver import build_module
 from ..runtime import ndarray as _nd
 from ..relay.backend import executor_factory
+from ..relay.backend.name_transforms import to_c_variable_style, prefix_generated_name
 from ..relay import param_dict
 from ..tir import expr
 
@@ -43,6 +44,20 @@ class UnsupportedInModelLibraryFormatError(Exception):
     """Raised when export_model_library_format does not support the given Module tree."""
 
 
+def generate_c_interface_header(module_name, inputs, outputs, include_path):
+    """Generate C Interface header to be included in MLF"""
+    mangled_name = to_c_variable_style(prefix_generated_name(module_name))
+    metadata_header = os.path.join(include_path, f"{mangled_name}.h")
+
+    interface_c_create = tvm._ffi.get_global_func("runtime.InterfaceCCreate")
+    interface_c_module = interface_c_create(module_name, inputs, outputs)
+
+    with open(metadata_header, "w") as header_file:
+        header_file.write(interface_c_module.get_source())
+
+    return metadata_header
+
+
 def _populate_codegen_dir(mod, codegen_dir: str, module_name: str = None):
     """Populate the codegen sub-directory as part of a Model Library Format export.
 
diff --git a/python/tvm/relay/backend/name_transforms.py b/python/tvm/relay/backend/name_transforms.py
--- a/python/tvm/relay/backend/name_transforms.py
+++ b/python/tvm/relay/backend/name_transforms.py
@@ -48,6 +48,18 @@ def to_c_variable_style(original_name: str):
     return _backend.ToCVariableStyle(original_name)
 
 
+def to_c_constant_style(original_name: str):
+    """Transform a name to the C constant style assuming it is
+    appropriately constructed using the prefixing functions
+
+    Parameters
+    ----------
+    original_name : str
+        Original name to transform
+    """
+    return _backend.ToCConstantStyle(original_name)
+
+
 def _preprocess_names(names: Union[List[str], str]):
     """Preprocesses name strings into format for C++ functions
 


Test patch:
diff --git a/tests/cpp/name_transforms_test.cc b/tests/cpp/name_transforms_test.cc
--- a/tests/cpp/name_transforms_test.cc
+++ b/tests/cpp/name_transforms_test.cc
@@ -42,6 +42,14 @@ TEST(NameTransforms, ToCVariableStyle) {
   EXPECT_THROW(ToCVariableStyle(""), InternalError);
 }
 
+TEST(NameTransforms, ToCConstantStyle) {
+  ASSERT_EQ(ToCConstantStyle("TVM_Woof"), "TVM_WOOF");
+  ASSERT_EQ(ToCConstantStyle("TVM_woof"), "TVM_WOOF");
+  ASSERT_EQ(ToCConstantStyle("TVM_woof_Woof"), "TVM_WOOF_WOOF");
+  EXPECT_THROW(ToCConstantStyle("Cake_Bakery"), InternalError);  // Incorrect prefix
+  EXPECT_THROW(ToCConstantStyle(""), InternalError);
+}
+
 TEST(NameTransforms, PrefixName) {
   ASSERT_EQ(PrefixName({"Woof"}), "TVM_Woof");
   ASSERT_EQ(PrefixName({"woof"}), "TVM_woof");
@@ -71,10 +79,10 @@ TEST(NameTransforms, CombineNames) {
 }
 
 TEST(NameTransforms, SanitizeName) {
-  ASSERT_EQ(SanitizeName("+_+ "), "_");
+  ASSERT_EQ(SanitizeName("+_+ "), "____");
   ASSERT_EQ(SanitizeName("input+"), "input_");
   ASSERT_EQ(SanitizeName("input-"), "input_");
-  ASSERT_EQ(SanitizeName("input++"), "input_");
+  ASSERT_EQ(SanitizeName("input++"), "input__");
   ASSERT_EQ(SanitizeName("woof:1"), "woof_1");
   EXPECT_THROW(SanitizeName(""), InternalError);
 }
diff --git a/tests/cpp/target/source/interface_c_test.cc b/tests/cpp/target/source/interface_c_test.cc
new file mode 100644
--- /dev/null
+++ b/tests/cpp/target/source/interface_c_test.cc
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <tvm/runtime/container/array.h>
+#include <tvm/runtime/container/string.h>
+#include <tvm/runtime/module.h>
+
+using ::testing::HasSubstr;
+
+namespace tvm {
+namespace codegen {
+
+runtime::Module InterfaceCCreate(std::string module_name, Array<String> inputs,
+                                 Array<String> outputs);
+
+namespace {
+
+TEST(InterfaceAPI, ContainsHeaderGuards) {
+  std::stringstream upper_header_guard;
+  std::stringstream lower_header_guard;
+
+  upper_header_guard << "#ifndef TVMGEN_ULTIMATE_CAT_SPOTTER_H_\n"
+                     << "#define TVMGEN_ULTIMATE_CAT_SPOTTER_H_\n"
+                     << "#include <stdint.h>\n\n"
+                     << "#ifdef __cplusplus\n"
+                     << "extern \"C\" {\n"
+                     << "#endif\n\n";
+
+  lower_header_guard << "\n#ifdef __cplusplus\n"
+                     << "}\n"
+                     << "#endif\n\n"
+                     << "#endif // TVMGEN_ULTIMATE_CAT_SPOTTER_H_\n";
+
+  runtime::Module test_module = InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(upper_header_guard.str()));
+  ASSERT_THAT(header_source, HasSubstr(lower_header_guard.str()));
+}
+
+TEST(InterfaceAPI, ContainsRunFunction) {
+  std::stringstream run_function;
+
+  run_function << "/*!\n"
+               << " * \\brief entrypoint function for TVM module \"ultimate_cat_spotter\"\n"
+               << " * \\param inputs Input tensors for the module \n"
+               << " * \\param outputs Output tensors for the module \n"
+               << " */\n"
+               << "int32_t tvmgen_ultimate_cat_spotter_run(\n"
+               << "  struct tvmgen_ultimate_cat_spotter_inputs* inputs,\n"
+               << "  struct tvmgen_ultimate_cat_spotter_outputs* outputs\n"
+               << ");\n";
+
+  runtime::Module test_module = InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(run_function.str()));
+}
+
+TEST(InterfaceAPI, ContainsInputStructSingle) {
+  std::stringstream input_struct;
+
+  input_struct << "/*!\n"
+               << " * \\brief Input tensor pointers for TVM module \"ultimate_cat_spotter\" \n"
+               << " */\n"
+               << "struct tvmgen_ultimate_cat_spotter_inputs {\n"
+               << "  void* input;\n"
+               << "};\n\n";
+
+  runtime::Module test_module = InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(input_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsInputStructMany) {
+  std::stringstream input_struct;
+
+  input_struct << "struct tvmgen_ultimate_cat_spotter_inputs {\n"
+               << "  void* input1;\n"
+               << "  void* input2;\n"
+               << "};\n\n";
+
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input1", "input2"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(input_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsInputStructSanitised) {
+  std::stringstream input_struct;
+
+  input_struct << "struct tvmgen_ultimate_cat_spotter_inputs {\n"
+               << "  void* input_1;\n"
+               << "  void* input_2;\n"
+               << "};\n\n";
+
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input+1", "input+2"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(input_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsInputStructClash) {
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input+", "input-"}, {"output"});
+  ASSERT_THROW(test_module->GetSource(), InternalError);
+}
+
+TEST(InterfaceAPI, ContainsOutputStructSingle) {
+  std::stringstream output_struct;
+
+  output_struct << "/*!\n"
+                << " * \\brief Output tensor pointers for TVM module \"ultimate_cat_spotter\" \n"
+                << " */\n"
+                << "struct tvmgen_ultimate_cat_spotter_outputs {\n"
+                << "  void* output;\n"
+                << "};\n\n";
+
+  runtime::Module test_module = InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(output_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsOutputStructMany) {
+  std::stringstream output_struct;
+
+  output_struct << "struct tvmgen_ultimate_cat_spotter_outputs {\n"
+                << "  void* output1;\n"
+                << "  void* output2;\n"
+                << "};\n\n";
+
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output1", "output2"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(output_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsOutputStructSanitised) {
+  std::stringstream output_struct;
+
+  output_struct << "struct tvmgen_ultimate_cat_spotter_outputs {\n"
+                << "  void* output_1;\n"
+                << "  void* output_2;\n"
+                << "};\n\n";
+
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output+1", "output-2"});
+  std::string header_source = test_module->GetSource();
+
+  ASSERT_THAT(header_source, HasSubstr(output_struct.str()));
+}
+
+TEST(InterfaceAPI, ContainsOutputStructClash) {
+  runtime::Module test_module =
+      InterfaceCCreate("ultimate_cat_spotter", {"input"}, {"output+", "output-"});
+  ASSERT_THROW(test_module->GetSource(), InternalError);
+}
+
+}  // namespace
+}  // namespace codegen
+}  // namespace tvm
diff --git a/tests/micro/zephyr/test_zephyr_aot.py b/tests/micro/zephyr/test_zephyr_aot.py
--- a/tests/micro/zephyr/test_zephyr_aot.py
+++ b/tests/micro/zephyr/test_zephyr_aot.py
@@ -32,7 +32,7 @@
 import tvm.relay as relay
 
 from tvm.contrib.download import download_testdata
-from tvm.micro.interface_api import generate_c_interface_header
+from tvm.micro.model_library_format import generate_c_interface_header
 
 import test_utils
 
diff --git a/tests/micro/zephyr/test_zephyr_armv7m.py b/tests/micro/zephyr/test_zephyr_armv7m.py
--- a/tests/micro/zephyr/test_zephyr_armv7m.py
+++ b/tests/micro/zephyr/test_zephyr_armv7m.py
@@ -34,7 +34,7 @@
 from tvm import relay
 
 from tvm.contrib.download import download_testdata
-from tvm.micro.interface_api import generate_c_interface_header
+from tvm.micro.model_library_format import generate_c_interface_header
 
 import conftest
 
diff --git a/tests/python/relay/aot/test_crt_aot.py b/tests/python/relay/aot/test_crt_aot.py
--- a/tests/python/relay/aot/test_crt_aot.py
+++ b/tests/python/relay/aot/test_crt_aot.py
@@ -22,7 +22,7 @@
 import pytest
 
 import tvm
-from tvm import relay
+from tvm import relay, TVMError
 from tvm.ir.module import IRModule
 from tvm.relay import testing, transform
 from tvm.relay.testing import byoc
@@ -613,7 +613,7 @@ def test_name_sanitiser_name_clash():
     inputs = {"input::-1": x_data, "input::-2": y_data, "input:--2": t_data}
     output_list = generate_ref_data(func, inputs)
 
-    with pytest.raises(ValueError, match="Sanitized input tensor name clash"):
+    with pytest.raises(TVMError, match="Sanitized input tensor name clash"):
         compile_and_run(
             AOTTestModel(module=IRModule.from_expr(func), inputs=inputs, outputs=output_list),
             test_runner,
diff --git a/tests/python/relay/test_name_transforms.py b/tests/python/relay/test_name_transforms.py
--- a/tests/python/relay/test_name_transforms.py
+++ b/tests/python/relay/test_name_transforms.py
@@ -19,6 +19,7 @@
 from tvm.relay.backend.name_transforms import (
     to_c_function_style,
     to_c_variable_style,
+    to_c_constant_style,
     prefix_name,
     prefix_generated_name,
     sanitize_name,
@@ -51,6 +52,17 @@ def test_to_c_variable_style():
         to_c_variable_style("")
 
 
+def test_to_c_constant_style():
+    assert to_c_constant_style("TVM_Woof") == "TVM_WOOF"
+    assert to_c_constant_style("TVM_woof") == "TVM_WOOF"
+    assert to_c_constant_style("TVM_woof_Woof") == "TVM_WOOF_WOOF"
+
+    with pytest.raises(TVMError, match="Constant not TVM prefixed"):
+        to_c_constant_style("Cake_Bakery")
+    with pytest.raises(TVMError):
+        to_c_constant_style("")
+
+
 def test_prefix_name():
     assert prefix_name("Woof") == "TVM_Woof"
     assert prefix_name(["Woof"]) == "TVM_Woof"
@@ -81,10 +93,10 @@ def test_prefix_generated_name():
 
 
 def test_sanitize_name():
-    assert sanitize_name("+_+ ") == "_"
+    assert sanitize_name("+_+ ") == "____"
     assert sanitize_name("input+") == "input_"
     assert sanitize_name("input-") == "input_"
-    assert sanitize_name("input++") == "input_"
+    assert sanitize_name("input++") == "input__"
     assert sanitize_name("woof:1") == "woof_1"
 
     with pytest.raises(TVMError, match="Name is empty"):