Add the namespace hierarchy to BindingsAndMetadata

This cl also adds a --namespaces_json flag to Crubit for writing a .json file with the namespace contents. Bazel support for it will be provided in a followup cl.

PiperOrigin-RevId: 471791828
diff --git a/rs_bindings_from_cc/BUILD b/rs_bindings_from_cc/BUILD
index 5fb2839..8a3110f 100644
--- a/rs_bindings_from_cc/BUILD
+++ b/rs_bindings_from_cc/BUILD
@@ -67,6 +67,7 @@
         ":bazel_types",
         ":cc_ir",
         ":cmdline",
+        ":collect_namespaces",
         ":generate_bindings_and_metadata",
         "//common:file_io",
         "//common:rust_allocator_shims",
@@ -88,6 +89,7 @@
         ":cc_collect_instantiations",
         ":cc_ir",
         ":cmdline",
+        ":collect_namespaces",
         ":ir_from_cc",
         ":src_code_gen",
         "//common:status_macros",
@@ -101,6 +103,7 @@
     deps = [
         ":cc_ir",
         ":cmdline",
+        ":collect_namespaces",
         ":generate_bindings_and_metadata",
         "//common:rust_allocator_shims",
         "//common:test_utils",
diff --git a/rs_bindings_from_cc/cmdline.cc b/rs_bindings_from_cc/cmdline.cc
index 50a6545..707ccac 100644
--- a/rs_bindings_from_cc/cmdline.cc
+++ b/rs_bindings_from_cc/cmdline.cc
@@ -57,6 +57,9 @@
           "[template instantiation mode only] output path for the JSON file "
           "with mapping from a template instantiation to a generated Rust "
           "struct name. This file is used by cc_template! macro expansion.");
+ABSL_FLAG(std::string, namespaces_out, "",
+          "(optional) output path for the JSON file containing the target's"
+          "namespace hierarchy.");
 
 namespace crubit {
 
@@ -78,7 +81,8 @@
 absl::StatusOr<Cmdline> Cmdline::Create() {
   return CreateFromArgs(
       absl::GetFlag(FLAGS_cc_out), absl::GetFlag(FLAGS_rs_out),
-      absl::GetFlag(FLAGS_ir_out), absl::GetFlag(FLAGS_crubit_support_path),
+      absl::GetFlag(FLAGS_ir_out), absl::GetFlag(FLAGS_namespaces_out),
+      absl::GetFlag(FLAGS_crubit_support_path),
       absl::GetFlag(FLAGS_rustfmt_exe_path),
       absl::GetFlag(FLAGS_rustfmt_config_path), absl::GetFlag(FLAGS_do_nothing),
       absl::GetFlag(FLAGS_public_headers),
@@ -89,9 +93,9 @@
 
 absl::StatusOr<Cmdline> Cmdline::CreateFromArgs(
     std::string cc_out, std::string rs_out, std::string ir_out,
-    std::string crubit_support_path, std::string rustfmt_exe_path,
-    std::string rustfmt_config_path, bool do_nothing,
-    std::vector<std::string> public_headers,
+    std::string namespaces_out, std::string crubit_support_path,
+    std::string rustfmt_exe_path, std::string rustfmt_config_path,
+    bool do_nothing, std::vector<std::string> public_headers,
     std::string targets_and_headers_str, std::vector<std::string> rust_sources,
     std::string instantiations_out) {
   Cmdline cmdline;
@@ -108,6 +112,8 @@
 
   cmdline.ir_out_ = std::move(ir_out);
 
+  cmdline.namespaces_out_ = std::move(namespaces_out);
+
   if (crubit_support_path.empty()) {
     return absl::InvalidArgumentError("please specify --crubit_support_path");
   }
diff --git a/rs_bindings_from_cc/cmdline.h b/rs_bindings_from_cc/cmdline.h
index b7fbd41..44091ca 100644
--- a/rs_bindings_from_cc/cmdline.h
+++ b/rs_bindings_from_cc/cmdline.h
@@ -26,17 +26,17 @@
   // `rs_out`, and so forth.
   static absl::StatusOr<Cmdline> CreateForTesting(
       std::string cc_out, std::string rs_out, std::string ir_out,
-      std::string crubit_support_path, std::string rustfmt_exe_path,
-      std::string rustfmt_config_path, bool do_nothing,
-      std::vector<std::string> public_headers,
+      std::string namespaces_out, std::string crubit_support_path,
+      std::string rustfmt_exe_path, std::string rustfmt_config_path,
+      bool do_nothing, std::vector<std::string> public_headers,
       std::string targets_and_headers_str,
       std::vector<std::string> rust_sources, std::string instantiations_out) {
     return CreateFromArgs(
         std::move(cc_out), std::move(rs_out), std::move(ir_out),
-        std::move(crubit_support_path), std::move(rustfmt_exe_path),
-        std::move(rustfmt_config_path), do_nothing, std::move(public_headers),
-        std::move(targets_and_headers_str), std::move(rust_sources),
-        std::move(instantiations_out));
+        std::move(namespaces_out), std::move(crubit_support_path),
+        std::move(rustfmt_exe_path), std::move(rustfmt_config_path), do_nothing,
+        std::move(public_headers), std::move(targets_and_headers_str),
+        std::move(rust_sources), std::move(instantiations_out));
   }
 
   Cmdline(const Cmdline&) = delete;
@@ -47,6 +47,7 @@
   absl::string_view cc_out() const { return cc_out_; }
   absl::string_view rs_out() const { return rs_out_; }
   absl::string_view ir_out() const { return ir_out_; }
+  absl::string_view namespaces_out() const { return namespaces_out_; }
   absl::string_view crubit_support_path() const { return crubit_support_path_; }
   absl::string_view rustfmt_exe_path() const { return rustfmt_exe_path_; }
   absl::string_view rustfmt_config_path() const { return rustfmt_config_path_; }
@@ -71,9 +72,9 @@
 
   static absl::StatusOr<Cmdline> CreateFromArgs(
       std::string cc_out, std::string rs_out, std::string ir_out,
-      std::string crubit_support_path, std::string rustfmt_exe_path,
-      std::string rustfmt_config_path, bool do_nothing,
-      std::vector<std::string> public_headers,
+      std::string namespaces_out, std::string crubit_support_path,
+      std::string rustfmt_exe_path, std::string rustfmt_config_path,
+      bool do_nothing, std::vector<std::string> public_headers,
       std::string targets_and_headers_str,
       std::vector<std::string> rust_sources, std::string instantiations_out);
 
@@ -93,6 +94,8 @@
 
   std::string instantiations_out_;
   std::vector<std::string> rust_sources_;
+
+  std::string namespaces_out_;
 };
 
 }  // namespace crubit
diff --git a/rs_bindings_from_cc/cmdline_test.cc b/rs_bindings_from_cc/cmdline_test.cc
index a4f753f..0753cd1 100644
--- a/rs_bindings_from_cc/cmdline_test.cc
+++ b/rs_bindings_from_cc/cmdline_test.cc
@@ -26,8 +26,8 @@
 absl::StatusOr<Cmdline> TestCmdline(std::vector<std::string> public_headers,
                                     const std::string& targets_and_headers) {
   return Cmdline::CreateForTesting(
-      "cc_out", "rs_out", "ir_out", "crubit_support_path", "rustfmt_exe_path",
-      "rustfmt_config_path",
+      "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
+      "rustfmt_exe_path", "rustfmt_config_path",
 
       /* do_nothing= */ false, std::move(public_headers),
       std::move(targets_and_headers), /* rust_sources= */ {},
@@ -37,16 +37,18 @@
 }  // namespace
 
 TEST(CmdlineTest, BasicCorrectInput) {
-  ASSERT_OK_AND_ASSIGN(Cmdline cmdline,
-                       Cmdline::CreateForTesting(
-                           "cc_out", "rs_out", "ir_out", "crubit_support_path",
-                           "rustfmt_exe_path", "rustfmt_config_path",
-                           /* do_nothing= */ false, {"h1"},
-                           R"([{"t": "t1", "h": ["h1", "h2"]}])", {"lib.rs"},
-                           "instantiations_out"));
+  ASSERT_OK_AND_ASSIGN(
+      Cmdline cmdline,
+      Cmdline::CreateForTesting("cc_out", "rs_out", "ir_out", "namespaces_out",
+                                "crubit_support_path", "rustfmt_exe_path",
+                                "rustfmt_config_path",
+                                /* do_nothing= */ false, {"h1"},
+                                R"([{"t": "t1", "h": ["h1", "h2"]}])",
+                                {"lib.rs"}, "instantiations_out"));
   EXPECT_EQ(cmdline.cc_out(), "cc_out");
   EXPECT_EQ(cmdline.rs_out(), "rs_out");
   EXPECT_EQ(cmdline.ir_out(), "ir_out");
+  EXPECT_EQ(cmdline.namespaces_out(), "namespaces_out");
   EXPECT_EQ(cmdline.crubit_support_path(), "crubit_support_path");
   EXPECT_EQ(cmdline.rustfmt_exe_path(), "rustfmt_exe_path");
   EXPECT_EQ(cmdline.rustfmt_config_path(), "rustfmt_config_path");
@@ -194,7 +196,7 @@
     {"t": "target1", "h": ["a.h", "b.h"]}
   ])";
   ASSERT_THAT(
-      (Cmdline::CreateForTesting("cc_out", "rs_out", "ir_out",
+      (Cmdline::CreateForTesting("cc_out", "rs_out", "ir_out", "namespaces_out",
                                  "crubit_support_path", "rustfmt_exe_path",
                                  "rustfmt_config_path",
                                  /* do_nothing= */ false, {"a.h"},
@@ -213,7 +215,7 @@
   ])";
   ASSERT_THAT(
       Cmdline::CreateForTesting(
-          "cc_out", "rs_out", "ir_out", "crubit_support_path",
+          "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
           "rustfmt_exe_path", "rustfmt_config_path",
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* rust_sources= */ {}, "instantiations_out"),
@@ -228,14 +230,15 @@
   constexpr absl::string_view kTargetsAndHeaders = R"([
     {"t": "target1", "h": ["a.h", "b.h"]}
   ])";
-  ASSERT_THAT(Cmdline::CreateForTesting(
-                  /* cc_out= */ "", "rs_out", "ir_out", "crubit_support_path",
-                  "rustfmt_exe_path", "rustfmt_config_path",
-                  /* do_nothing= */ false, {"a.h"},
-                  std::string(kTargetsAndHeaders), /* rust_sources= */ {},
-                  /* instantiations_out= */ ""),
-              StatusIs(absl::StatusCode::kInvalidArgument,
-                       HasSubstr("please specify --cc_out")));
+  ASSERT_THAT(
+      Cmdline::CreateForTesting(
+          /* cc_out= */ "", "rs_out", "ir_out", "namespaces_out",
+          "crubit_support_path", "rustfmt_exe_path", "rustfmt_config_path",
+          /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
+          /* rust_sources= */ {},
+          /* instantiations_out= */ ""),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("please specify --cc_out")));
 }
 
 TEST(CmdlineTest, RsOutEmpty) {
@@ -244,8 +247,8 @@
   ])";
   ASSERT_THAT(
       Cmdline::CreateForTesting(
-          "cc_out", /* rs_out= */ "", "ir_out", "crubit_support_path",
-          "rustfmt_exe_path", "rustfmt_config_path",
+          "cc_out", /* rs_out= */ "", "namespaces_out", "ir_out",
+          "crubit_support_path", "rustfmt_exe_path", "rustfmt_config_path",
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* rust_sources= */ {},
           /* instantiations_out= */ ""),
@@ -258,8 +261,8 @@
     {"t": "target1", "h": ["a.h", "b.h"]}
   ])";
   ASSERT_OK(Cmdline::CreateForTesting(
-      "cc_out", "rs_out", /* ir_out= */ "", "crubit_support_path",
-      "rustfmt_exe_path", "rustfmt_config_path",
+      "cc_out", "rs_out", /* ir_out= */ "", "namespaces_out",
+      "crubit_support_path", "rustfmt_exe_path", "rustfmt_config_path",
       /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
       /* rust_sources= */ {},
       /* instantiations_out= */ ""));
@@ -271,7 +274,7 @@
   ])";
   ASSERT_THAT(
       Cmdline::CreateForTesting(
-          "cc_out", "rs_out", "ir_out", "crubit_support_path",
+          "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
           /* rustfmt_exe_path= */ "", "rustfmt_config_path",
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* rust_sources= */ {},
diff --git a/rs_bindings_from_cc/collect_namespaces.cc b/rs_bindings_from_cc/collect_namespaces.cc
index b921496..8b2bfd0 100644
--- a/rs_bindings_from_cc/collect_namespaces.cc
+++ b/rs_bindings_from_cc/collect_namespaces.cc
@@ -79,14 +79,14 @@
     }
   }
 
-  // Converts a trie node into the JSON serializable NamespaceForCcImport.
-  NamespaceForCcImport NodeToNamespaceForCcImport(const Node* node) const {
-    std::vector<NamespaceForCcImport> namespaces;
+  // Converts a trie node into the JSON serializable NamespaceNode.
+  NamespaceNode NodeToNamespaceNode(const Node* node) const {
+    std::vector<NamespaceNode> namespaces;
     namespaces.reserve(node->child_name_to_idx.size());
     for (const auto& [_, idx] : node->child_name_to_idx) {
-      namespaces.push_back(NodeToNamespaceForCcImport(&trie_nodes_[idx]));
+      namespaces.push_back(NodeToNamespaceNode(&trie_nodes_[idx]));
     }
-    return NamespaceForCcImport{std::string(node->name), std::move(namespaces)};
+    return NamespaceNode{std::string(node->name), std::move(namespaces)};
   }
 
  public:
@@ -118,21 +118,21 @@
     }
   }
 
-  // Converts the trie into the JSON serializable AllNamespacesForCcImport.
-  AllNamespacesForCcImport ToAllNamespacesForCcImport() {
-    std::vector<NamespaceForCcImport> namespaces;
+  // Converts the trie into the JSON serializable NamespacesHierarchy.
+  NamespacesHierarchy ToNamespacesHierarchy() {
+    std::vector<NamespaceNode> namespaces;
     namespaces.reserve(this->top_level_name_to_idx_.size());
     for (auto& [_, idx] : this->top_level_name_to_idx_) {
-      namespaces.push_back(NodeToNamespaceForCcImport(&trie_nodes_[idx]));
+      namespaces.push_back(NodeToNamespaceNode(&trie_nodes_[idx]));
     }
-    return AllNamespacesForCcImport{std::move(namespaces)};
+    return NamespacesHierarchy{std::move(namespaces)};
   }
 };
 
 }  // namespace
 
 // Returns the current target's namespace hierarchy in JSON serializable format.
-AllNamespacesForCcImport CollectNamespaces(const IR& ir) {
+NamespacesHierarchy CollectNamespaces(const IR& ir) {
   auto all_namespaces = ir.get_items_if<Namespace>();
   absl::flat_hash_map<ItemId, const Namespace*> id_to_namespace;
   for (auto ns : all_namespaces) {
@@ -152,10 +152,10 @@
     trie.InsertTopLevel(ns);
   }
 
-  return trie.ToAllNamespacesForCcImport();
+  return trie.ToNamespacesHierarchy();
 }
 
-llvm::json::Value NamespaceForCcImport::ToJson() const {
+llvm::json::Value NamespaceNode::ToJson() const {
   std::vector<llvm::json::Value> json_children;
   json_children.reserve(children.size());
   for (const auto& child : children) {
@@ -172,7 +172,7 @@
   };
 }
 
-llvm::json::Value AllNamespacesForCcImport::ToJson() const {
+llvm::json::Value NamespacesHierarchy::ToJson() const {
   std::vector<llvm::json::Value> json_namespaces;
   json_namespaces.reserve(namespaces.size());
   for (const auto& ns : namespaces) {
diff --git a/rs_bindings_from_cc/collect_namespaces.h b/rs_bindings_from_cc/collect_namespaces.h
index 6ef5cc1..d244ed5 100644
--- a/rs_bindings_from_cc/collect_namespaces.h
+++ b/rs_bindings_from_cc/collect_namespaces.h
@@ -17,34 +17,33 @@
 // stores the names of the namespace children, as it is the only information
 // that the cc_import! macro needs in order to be able to merge namespaces
 // across targets.
-struct NamespaceForCcImport {
+struct NamespaceNode {
   llvm::json::Value ToJson() const;
 
   std::string name;
-  std::vector<NamespaceForCcImport> children;
+  std::vector<NamespaceNode> children;
 };
 
-inline std::ostream& operator<<(std::ostream& o,
-                                const NamespaceForCcImport& ns) {
+inline std::ostream& operator<<(std::ostream& o, const NamespaceNode& ns) {
   return o << std::string(llvm::formatv("{0:2}", ns.ToJson()));
 }
 
 // Representation of all C++ namespaces within the current target.
-struct AllNamespacesForCcImport {
+struct NamespacesHierarchy {
   llvm::json::Value ToJson() const;
 
-  std::vector<NamespaceForCcImport> namespaces;
+  std::vector<NamespaceNode> namespaces;
 };
 
 inline std::ostream& operator<<(std::ostream& o,
-                                const AllNamespacesForCcImport& all) {
+                                const NamespacesHierarchy& all) {
   return o << std::string(llvm::formatv("{0:2}", all.ToJson()));
 }
 
 // Returns the current target's namespace hierarchy in JSON serializable format.
-AllNamespacesForCcImport CollectNamespaces(const IR& ir);
+NamespacesHierarchy CollectNamespaces(const IR& ir);
 
-inline std::string NamespacesAsJson(const AllNamespacesForCcImport& topLevel) {
+inline std::string NamespacesAsJson(const NamespacesHierarchy& topLevel) {
   return llvm::formatv("{0:2}", topLevel.ToJson());
 }
 }  // namespace crubit
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata.cc b/rs_bindings_from_cc/generate_bindings_and_metadata.cc
index 90d6036..d43144e 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata.cc
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata.cc
@@ -6,6 +6,7 @@
 
 #include "common/status_macros.h"
 #include "rs_bindings_from_cc/collect_instantiations.h"
+#include "rs_bindings_from_cc/collect_namespaces.h"
 #include "rs_bindings_from_cc/ir_from_cc.h"
 #include "rs_bindings_from_cc/src_code_gen.h"
 
@@ -36,10 +37,13 @@
                                cmdline.rustfmt_exe_path(),
                                cmdline.rustfmt_config_path()));
 
+  auto top_level_namespaces = crubit::CollectNamespaces(ir);
+
   return BindingsAndMetadata{
       .ir = ir,
       .rs_api = bindings.rs_api,
       .rs_api_impl = bindings.rs_api_impl,
+      .namespaces = top_level_namespaces,
   };
 }
 
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata.h b/rs_bindings_from_cc/generate_bindings_and_metadata.h
index 5140c0a..b641d9d 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata.h
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata.h
@@ -10,6 +10,7 @@
 
 #include "absl/status/statusor.h"
 #include "rs_bindings_from_cc/cmdline.h"
+#include "rs_bindings_from_cc/collect_namespaces.h"
 #include "rs_bindings_from_cc/ir.h"
 
 namespace crubit {
@@ -22,6 +23,8 @@
   std::string rs_api;
   // Generated C++ source code.
   std::string rs_api_impl;
+  // A hierarchy tree for all C++ namespaces used in the target.
+  NamespacesHierarchy namespaces;
 };
 
 // Returns `BindingsAndMetadata` as requested by the user on the command line.
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc b/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
index c89fb8c..193488a 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
@@ -8,6 +8,7 @@
 #include "gtest/gtest.h"
 #include "common/test_utils.h"
 #include "rs_bindings_from_cc/cmdline.h"
+#include "rs_bindings_from_cc/collect_namespaces.h"
 #include "rs_bindings_from_cc/ir.h"
 
 namespace crubit {
@@ -26,7 +27,7 @@
   ASSERT_OK_AND_ASSIGN(
       Cmdline cmdline,
       Cmdline::CreateForTesting(
-          "cc_out", "rs_out", "ir_out", "crubit_support_path",
+          "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
           std::string(kDefaultRustfmtExePath), "nowhere/rustfmt.toml",
           /* do_nothing= */ false,
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
@@ -54,7 +55,7 @@
   ASSERT_OK_AND_ASSIGN(
       Cmdline cmdline,
       Cmdline::CreateForTesting(
-          "cc_out", "rs_out", "ir_out", "crubit_support_path",
+          "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
           std::string(kDefaultRustfmtExePath), "nowhere/rustfmt.toml",
           /* do_nothing= */ false,
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
@@ -79,7 +80,7 @@
   ASSERT_OK_AND_ASSIGN(
       Cmdline cmdline,
       Cmdline::CreateForTesting(
-          "cc_out", "rs_out", "ir_out", "crubit_support_path",
+          "cc_out", "rs_out", "ir_out", "namespaces_out", "crubit_support_path",
           std::string(kDefaultRustfmtExePath), "nowhere/rustfmt.toml",
           /* do_nothing= */ false,
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
@@ -96,5 +97,87 @@
   ASSERT_THAT(InstantiationsAsJson(result.ir), StrEq("{}"));
 }
 
+TEST(GenerateBindingsAndMetadataTest, NamespacesJsonGenerated) {
+  constexpr absl::string_view kTargetsAndHeaders = R"([
+    {"t": "target1", "h": ["a.h"]}
+  ])";
+  constexpr absl::string_view kHeaderContent = R"(
+    namespace top_level_1 {
+      namespace middle {
+        namespace inner_1 {}
+      }
+      namespace middle {
+        namespace inner_2 {}
+      }
+    }
+
+    namespace top_level_2 {
+      namespace inner_3 {}
+    }
+
+    namespace top_level_1 {}
+  )";
+  constexpr absl::string_view kExpected = R"({
+  "namespaces": [
+    {
+      "namespace": {
+        "children": [
+          {
+            "namespace": {
+              "children": [
+                {
+                  "namespace": {
+                    "children": [],
+                    "name": "inner_1"
+                  }
+                },
+                {
+                  "namespace": {
+                    "children": [],
+                    "name": "inner_2"
+                  }
+                }
+              ],
+              "name": "middle"
+            }
+          }
+        ],
+        "name": "top_level_1"
+      }
+    },
+    {
+      "namespace": {
+        "children": [
+          {
+            "namespace": {
+              "children": [],
+              "name": "inner_3"
+            }
+          }
+        ],
+        "name": "top_level_2"
+      }
+    }
+  ]
+})";
+
+  ASSERT_OK_AND_ASSIGN(
+      Cmdline cmdline,
+      Cmdline::CreateForTesting(
+          "cc_out", "rs_out", "ir_out", "namespaces_json",
+          "crubit_support_path", std::string(kDefaultRustfmtExePath),
+          "nowhere/rustfmt.toml",
+          /* do_nothing= */ false,
+          /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
+          /* rust_sources= */ {}, /* instantiations_out= */ ""));
+  ASSERT_OK_AND_ASSIGN(BindingsAndMetadata result,
+                       GenerateBindingsAndMetadata(
+                           cmdline, DefaultClangArgs(),
+                           /* virtual_headers_contents= */
+                           {{HeaderName("a.h"), std::string(kHeaderContent)}}));
+
+  ASSERT_THAT(NamespacesAsJson(result.namespaces), StrEq(kExpected));
+}
+
 }  // namespace
 }  // namespace crubit
diff --git a/rs_bindings_from_cc/rs_bindings_from_cc.cc b/rs_bindings_from_cc/rs_bindings_from_cc.cc
index 1383fdc..fef6583 100644
--- a/rs_bindings_from_cc/rs_bindings_from_cc.cc
+++ b/rs_bindings_from_cc/rs_bindings_from_cc.cc
@@ -17,7 +17,9 @@
 #include "common/file_io.h"
 #include "common/status_macros.h"
 #include "rs_bindings_from_cc/cmdline.h"
+#include "rs_bindings_from_cc/collect_namespaces.h"
 #include "rs_bindings_from_cc/generate_bindings_and_metadata.h"
+#include "rs_bindings_from_cc/ir.h"
 #include "llvm/Support/raw_ostream.h"
 
 namespace crubit {
@@ -38,6 +40,9 @@
       CRUBIT_RETURN_IF_ERROR(
           SetFileContents(cmdline.instantiations_out(), "[]"));
     }
+    if (!cmdline.namespaces_out().empty()) {
+      CRUBIT_RETURN_IF_ERROR(SetFileContents(cmdline.namespaces_out(), "[]"));
+    }
     return absl::OkStatus();
   }
 
@@ -64,6 +69,12 @@
         crubit::InstantiationsAsJson(bindings_and_metadata.ir)));
   }
 
+  if (!cmdline.namespaces_out().empty()) {
+    CRUBIT_RETURN_IF_ERROR(SetFileContents(
+        cmdline.namespaces_out(),
+        crubit::NamespacesAsJson(bindings_and_metadata.namespaces)));
+  }
+
   return absl::OkStatus();
 }