Generate rudimentary Rust mojom bindings

Create a mojom binding generator for Rust. The generated bindings are
not useful yet, and only even compile for basic structs. However, this
is a good starting point for iteration.

A simple test in Rust is added to check that the Rect bindings build
and have the expected fields.

Bug: 1274864
Change-Id: I4107e26c50d2ddb36067a0238d7d2e72aeecd9d2
Cq-Include-Trybots: luci.chromium.try:android-rust-arm-dbg,android-rust-arm-rel,linux-rust-x64-dbg,linux-rust-x64-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4198179
Auto-Submit: Collin Baker <collinbaker@chromium.org>
Reviewed-by: Hans Wennborg <hans@chromium.org>
Commit-Queue: Hans Wennborg <hans@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1100917}
diff --git a/build/rust/rust_target.gni b/build/rust/rust_target.gni
index eaa318f..c25d7d32 100644
--- a/build/rust/rust_target.gni
+++ b/build/rust/rust_target.gni
@@ -156,6 +156,13 @@
         invoker.build_native_rust_unit_tests && can_build_rust_unit_tests
   }
 
+  _test_deps = []
+  if (_build_unit_tests && defined(invoker.test_deps)) {
+    _test_deps = invoker.test_deps
+  } else if (defined(invoker.test_deps)) {
+    not_needed(invoker, [ "test_deps" ])
+  }
+
   # Declares that the Rust crate generates bindings between C++ and Rust via the
   # Cxx crate. It may generate C++ headers and/or use the cxx crate macros to
   # generate Rust code internally, depending on what bindings are declared. If
@@ -348,9 +355,7 @@
         deps = _rust_deps + _public_deps
         aliased_deps = _rust_aliased_deps
         public_deps = [ ":${_target_name}" ]
-        if (defined(invoker.test_deps)) {
-          deps += invoker.test_deps
-        }
+        deps += _test_deps
         inputs = []
         if (defined(invoker.inputs)) {
           inputs += invoker.inputs
@@ -369,6 +374,7 @@
                    "_crate_root",
                    "_crate_name",
                    "_metadata",
+                   "_test_deps",
                  ])
     }
   }
diff --git a/build/toolchain/gcc_toolchain.gni b/build/toolchain/gcc_toolchain.gni
index 713c1b9..374b3fe4 100644
--- a/build/toolchain/gcc_toolchain.gni
+++ b/build/toolchain/gcc_toolchain.gni
@@ -710,6 +710,7 @@
       rustc = "$rust_compiler_prefix${rustc_bin}"
       rust_sysroot_relative_to_out = rebase_path(rust_sysroot, root_out_dir)
       rustc_wrapper = rebase_path("//build/rust/rustc_wrapper.py")
+      gen_dir = rebase_path(root_gen_dir)
 
       # RSP manipulation due to https://bugs.chromium.org/p/gn/issues/detail?id=249
       tool("rust_staticlib") {
@@ -717,7 +718,7 @@
         depfile = "{{output}}.d"
         rspfile = "$rust_outfile.rsp"
         rspfile_content = "{{rustdeps}} {{externs}}"
-        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS RUSTENV {{rustenv}}"
+        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS RUSTENV GEN_DIR=\"$gen_dir\" {{rustenv}}"
         description = "RUST $rust_outfile"
         rust_sysroot = rust_sysroot_relative_to_out
         outputs = [ rust_outfile ]
@@ -730,7 +731,7 @@
         # Do not use rsp files in this (common) case because they occupy the
         # ninja main thread, and {{rlibs}} have shorter command lines than
         # fully linked targets.
-        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args {{rustdeps}} {{externs}} --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS RUSTENV {{rustenv}}"
+        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args {{rustdeps}} {{externs}} --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS RUSTENV GEN_DIR=\"$gen_dir\" {{rustenv}}"
         description = "RUST $rust_outfile"
         rust_sysroot = rust_sysroot_relative_to_out
         outputs = [ rust_outfile ]
@@ -741,7 +742,7 @@
         depfile = "{{output}}.d"
         rspfile = "$rust_outfile.rsp"
         rspfile_content = "{{rustdeps}} {{externs}}"
-        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV {{rustenv}}"
+        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV GEN_DIR=\"$gen_dir\" {{rustenv}}"
         description = "RUST $rust_outfile"
         rust_sysroot = rust_sysroot_relative_to_out
         outputs = [ rust_outfile ]
@@ -752,7 +753,7 @@
         depfile = "{{output}}.d"
         rspfile = "$rust_outfile.rsp"
         rspfile_content = "{{rustdeps}} {{externs}}"
-        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV {{rustenv}}"
+        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV GEN_DIR=\"$gen_dir\" {{rustenv}}"
         description = "RUST $rust_outfile"
         rust_sysroot = rust_sysroot_relative_to_out
         outputs = [ rust_outfile ]
@@ -763,7 +764,7 @@
         depfile = "{{output}}.d"
         rspfile = "$rust_outfile.rsp"
         rspfile_content = "{{rustdeps}} {{externs}}"
-        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV {{rustenv}}"
+        command = "$python_path \"$rustc_wrapper\" --rustc=$rustc --depfile=$depfile --rsp=$rspfile -- -Clinker=\"${invoker.cxx}\" $rustc_common_args --emit=dep-info=$depfile,link -o $rust_outfile LDFLAGS {{ldflags}} ${extra_ldflags} RUSTENV GEN_DIR=\"$gen_dir\" {{rustenv}}"
         description = "RUST $rust_outfile"
         rust_sysroot = rust_sysroot_relative_to_out
         outputs = [ rust_outfile ]
diff --git a/mojo/public/rust/BUILD.gn b/mojo/public/rust/BUILD.gn
index f9a96d99..3645073 100644
--- a/mojo/public/rust/BUILD.gn
+++ b/mojo/public/rust/BUILD.gn
@@ -85,6 +85,11 @@
     "//third_party/rust/bitflags/v1:lib",
   ]
 
+  # TODO(crbug.com/1274864): find a better way to reference generated bindings.
+  # Right now a gen/ file is `include!`ed directly in Rust, meaning there's no
+  # way to detect dependency issues.
+  test_deps = [ "//mojo/public/interfaces/bindings/tests:test_interfaces" ]
+
   bindgen_output = get_target_outputs(":mojo_c_system_binding")
   inputs = bindgen_output
   rustenv = [ "BINDGEN_RS_FILE=" + rebase_path(bindgen_output[0]) ]
diff --git a/mojo/public/rust/bindings/mod.rs b/mojo/public/rust/bindings/mod.rs
index e90fb52..61cb583 100644
--- a/mojo/public/rust/bindings/mod.rs
+++ b/mojo/public/rust/bindings/mod.rs
@@ -11,3 +11,6 @@
 pub mod message;
 pub mod mojom;
 pub mod run_loop;
+
+#[cfg(test)]
+mod tests;
diff --git a/mojo/public/rust/bindings/tests/basic_unittest.rs b/mojo/public/rust/bindings/tests/basic_unittest.rs
new file mode 100644
index 0000000..406338a8bf
--- /dev/null
+++ b/mojo/public/rust/bindings/tests/basic_unittest.rs
@@ -0,0 +1,18 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Simply test that we can compile some generated Rust struct bindings and that
+//! they have the expected fields.
+
+// TODO(crbug.com/1274864): find a better way to reference generated bindings.
+// This is not sustainable, but it's sufficient for a single test while
+// prototyping.
+mod rect_mojom {
+    include!(concat!(env!("GEN_DIR"), "/mojo/public/interfaces/bindings/tests/rect.mojom.rs"));
+}
+
+#[test]
+fn basic_struct_test() {
+    let _rect = rect_mojom::Rect { x: 1, y: 1, width: 1, height: 1 };
+}
diff --git a/mojo/public/rust/bindings/tests/mod.rs b/mojo/public/rust/bindings/tests/mod.rs
new file mode 100644
index 0000000..d40dfe2
--- /dev/null
+++ b/mojo/public/rust/bindings/tests/mod.rs
@@ -0,0 +1,5 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+pub mod basic_unittest;
diff --git a/mojo/public/tools/bindings/BUILD.gn b/mojo/public/tools/bindings/BUILD.gn
index 59700cb..3bf2e32d 100644
--- a/mojo/public/tools/bindings/BUILD.gn
+++ b/mojo/public/tools/bindings/BUILD.gn
@@ -90,6 +90,8 @@
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_macros.tmpl",
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_to_proto_macros.tmpl",
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_traits_specialization_macros.tmpl",
+    "$mojom_generator_root/generators/rust_templates/module.tmpl",
+    "$mojom_generator_root/generators/rust_templates/struct.tmpl",
     "$mojom_generator_root/generators/ts_templates/enum_definition.tmpl",
     "$mojom_generator_root/generators/ts_templates/interface_definition.tmpl",
     "$mojom_generator_root/generators/ts_templates/module_definition.tmpl",
@@ -105,6 +107,7 @@
     "$target_gen_dir/js_interface_binder_templates.zip",
     "$target_gen_dir/js_templates.zip",
     "$target_gen_dir/mojolpm_templates.zip",
+    "$target_gen_dir/rust_templates.zip",
     "$target_gen_dir/ts_templates.zip",
   ]
   args = [
diff --git a/mojo/public/tools/bindings/generators/OWNERS b/mojo/public/tools/bindings/generators/OWNERS
index fb33ab8..170c0f8 100644
--- a/mojo/public/tools/bindings/generators/OWNERS
+++ b/mojo/public/tools/bindings/generators/OWNERS
@@ -1,2 +1,5 @@
 # TypeScript bindings generator
 per-file mojom_ts_generator.py=rbpotter@chromium.org
+
+# Rust bindings generator
+per-file mojom_rust_generator.py=file://build/rust/OWNERS
diff --git a/mojo/public/tools/bindings/generators/mojom_rust_generator.py b/mojo/public/tools/bindings/generators/mojom_rust_generator.py
new file mode 100644
index 0000000..238ddbb24
--- /dev/null
+++ b/mojo/public/tools/bindings/generators/mojom_rust_generator.py
@@ -0,0 +1,67 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import mojom.generate.generator as generator
+import mojom.generate.module as mojom
+import mojom.generate.pack as pack
+from mojom.generate.template_expander import UseJinja, UseJinjaForImportedTemplate
+
+_kind_to_rust_type = {
+    mojom.BOOL: "bool",
+    mojom.INT8: "i8",
+    mojom.INT16: "i16",
+    mojom.INT32: "i32",
+    mojom.INT64: "i64",
+    mojom.UINT8: "u8",
+    mojom.UINT16: "u16",
+    mojom.UINT32: "u32",
+    mojom.UINT64: "u64",
+    mojom.FLOAT: "f32",
+    mojom.DOUBLE: "f64",
+}
+
+
+class Generator(generator.Generator):
+  def __init__(self, *args, **kwargs):
+    super(Generator, self).__init__(*args, **kwargs)
+
+  def _GetNameForKind(self, kind):
+    "::".join(kind.module.namespace.split("."))
+
+  def _GetRustFieldType(self, kind):
+    if mojom.IsEnumKind(kind) or mojom.IsStructKind(kind) or mojom.IsUnionKind(
+        kind):
+      return self._GetNameForKind(kind)
+    if mojom.IsArrayKind(kind):
+      return f"Vec<{self._GetRustFieldType(kind.kind)}>"
+    if mojom.IsMapKind(kind):
+      return (f"HashMap<"
+              f"{self._GetRustFieldType(kind.key_kind)}, "
+              f"{self._GetRustFieldType(kind.value_kind)}>")
+    if mojom.IsStringKind(kind):
+      return "String"
+    if mojom.IsAnyHandleKind(kind):
+      return "::mojo::UntypedHandle"
+    if mojom.IsReferenceKind(kind):
+      return "usize"
+    return _kind_to_rust_type[kind]
+
+  @staticmethod
+  def GetTemplatePrefix():
+    return "rust_templates"
+
+  def GetFilters(self):
+    rust_filters = {
+        "rust_field_type": self._GetRustFieldType,
+    }
+    return rust_filters
+
+  @UseJinja("module.tmpl")
+  def _GenerateModule(self):
+    return {"module": self.module}
+
+  def GenerateFiles(self, args):
+    self.module.Stylize(generator.Stylizer())
+
+    self.WriteWithComment(self._GenerateModule(), f"{self.module.path}.rs")
diff --git a/mojo/public/tools/bindings/generators/rust_templates/OWNERS b/mojo/public/tools/bindings/generators/rust_templates/OWNERS
new file mode 100644
index 0000000..ec1b1412
--- /dev/null
+++ b/mojo/public/tools/bindings/generators/rust_templates/OWNERS
@@ -0,0 +1 @@
+file://build/rust/OWNERS
diff --git a/mojo/public/tools/bindings/generators/rust_templates/module.tmpl b/mojo/public/tools/bindings/generators/rust_templates/module.tmpl
new file mode 100644
index 0000000..46ccdbb
--- /dev/null
+++ b/mojo/public/tools/bindings/generators/rust_templates/module.tmpl
@@ -0,0 +1,3 @@
+{%- for struct in module.structs %}
+{%   include "struct.tmpl" %}
+{% endfor %}
diff --git a/mojo/public/tools/bindings/generators/rust_templates/struct.tmpl b/mojo/public/tools/bindings/generators/rust_templates/struct.tmpl
new file mode 100644
index 0000000..bc2edd9e
--- /dev/null
+++ b/mojo/public/tools/bindings/generators/rust_templates/struct.tmpl
@@ -0,0 +1,6 @@
+#[derive(Debug)]
+pub struct {{struct.name}} {
+{%- for packed_field in struct.packed.packed_fields %}
+    pub {{packed_field.field.name}}: {{packed_field.field.kind|rust_field_type}},
+{%- endfor %}
+}
diff --git a/mojo/public/tools/bindings/mojom.gni b/mojo/public/tools/bindings/mojom.gni
index 43ff9f1..9c7454e 100644
--- a/mojo/public/tools/bindings/mojom.gni
+++ b/mojo/public/tools/bindings/mojom.gni
@@ -18,6 +18,7 @@
 import("//build/config/chromeos/ui_mode.gni")
 import("//build/config/features.gni")
 import("//build/config/nacl/config.gni")
+import("//build/config/rust.gni")
 import("//build/toolchain/kythe.gni")
 import("//components/nacl/features.gni")
 import("//third_party/jinja2/jinja2.gni")
@@ -44,6 +45,9 @@
   # MojoLPM fuzzer targets. Off by default.
   enable_mojom_fuzzer = false
 
+  # Enables generating mojom bindings for Rust targets.
+  enable_rust_bindings = enable_rust
+
   # Enables Closure compilation of generated JS lite bindings. In environments
   # where compilation is supported, any mojom target "foo" will also have a
   # corresponding "foo_js_library_for_compile" target generated.
@@ -124,6 +128,11 @@
       "$mojom_generator_script",
     ]
 
+if (enable_rust) {
+  mojom_generator_sources +=
+      [ "$mojom_generator_root/generators/mojom_rust_generator.py" ]
+}
+
 if (enable_scrambled_message_ids) {
   declare_args() {
     # The path to a file whose contents can be used as the basis for a message
@@ -1622,6 +1631,51 @@
       sources_target_name = output_target_name
     }
 
+    if (enable_rust_bindings) {
+      rust_generator_target_name = "${output_target_name}_rust__generator"
+
+      if (sources_list != []) {
+        action(rust_generator_target_name) {
+          script = mojom_generator_script
+          inputs = mojom_generator_sources + jinja2_sources
+          sources = sources_list
+
+          deps = [
+            ":$parser_target_name",
+            "//mojo/public/tools/bindings:precompile_templates",
+          ]
+          if (defined(invoker.parser_deps)) {
+            deps += invoker.parser_deps
+          }
+
+          args = common_generator_args
+          filelist = []
+          outputs = []
+          foreach(source, sources_list) {
+            filelist += [ rebase_path("$source", root_build_dir) ]
+          }
+          foreach(base_path, output_file_base_paths) {
+            outputs += [ "$root_gen_dir/${base_path}${variant_suffix}.rs" ]
+          }
+
+          response_file_contents = filelist
+
+          args += [
+            "--filelist={{response_file_name}}",
+            "-g",
+            "rust",
+          ]
+        }
+      } else {
+        group(rust_generator_target_name) {
+        }
+      }
+
+      group("${output_target_name}_rust") {
+        deps = [ ":$rust_generator_target_name" ]
+      }
+    }
+
     target(sources_target_type, sources_target_name) {
       if (defined(output_name_override)) {
         output_name = output_name_override
@@ -1649,6 +1703,11 @@
         ":$shared_cpp_library_target_name",
         "//base",
       ]
+
+      if (enable_rust_bindings) {
+        deps += [ ":${output_target_name}_rust" ]
+      }
+
       if (require_full_cpp_deps) {
         public_deps += [ "//mojo/public/cpp/bindings" ]
       } else {
diff --git a/mojo/public/tools/bindings/mojom_bindings_generator.py b/mojo/public/tools/bindings/mojom_bindings_generator.py
index 80cbf46..7edfcca 100755
--- a/mojo/public/tools/bindings/mojom_bindings_generator.py
+++ b/mojo/public/tools/bindings/mojom_bindings_generator.py
@@ -55,6 +55,7 @@
     "javascript": "mojom_js_generator",
     "java": "mojom_java_generator",
     "mojolpm": "mojom_mojolpm_generator",
+    "rust": "mojom_rust_generator",
     "typescript": "mojom_ts_generator",
 }