Switch FFI method to cxxbridge, and add tests to ffi crate.

cxxbridge gives us several valuable features:
* Conversions over the FFI layer are type-checked
* Avoid unsafe
* Code is much more succinct and very easy to read

Bug: 528225104
Change-Id: I931126d07d8c435ee9e9e6cfc74f80c76a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/23680
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/build/gen.py b/build/gen.py
index ba6f170..6f13c8c 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -730,9 +730,7 @@
               'src/gn/err.cc',
               'src/gn/escape.cc',
               'src/gn/exec_process.cc',
-              'src/gn/ffi/label.cc',
-              'src/gn/ffi/output_file.cc',
-              'src/gn/ffi/settings.cc',
+              'src/gn/ffi/bridge.cc',
               'src/gn/filesystem_utils.cc',
               'src/gn/file_writer.cc',
               'src/gn/frameworks_utils.cc',
@@ -854,6 +852,7 @@
               'src/util/sys_info.cc',
               'src/util/ticks.cc',
               'src/util/worker_pool.cc',
+              'src/gn/test_with_scope.cc',
           ]
       },
   }
@@ -952,7 +951,6 @@
         'src/gn/target_unittest.cc',
         'src/gn/template_unittest.cc',
         'src/gn/test_with_scheduler.cc',
-        'src/gn/test_with_scope.cc',
         'src/gn/tokenizer_unittest.cc',
         'src/gn/unique_vector_unittest.cc',
         'src/gn/value_unittest.cc',
diff --git a/src/gn/ffi/.gitignore b/src/gn/ffi/.gitignore
new file mode 100644
index 0000000..6adb822
--- /dev/null
+++ b/src/gn/ffi/.gitignore
@@ -0,0 +1 @@
+.bin/
\ No newline at end of file
diff --git a/src/gn/ffi/bridge.cc b/src/gn/ffi/bridge.cc
new file mode 100644
index 0000000..81988fc
--- /dev/null
+++ b/src/gn/ffi/bridge.cc
@@ -0,0 +1,222 @@
+// This file is generated by src/gn/ffi/update_bridge.sh. Do not edit manually.
+// Source: src/gn/ffi/starlark/crates/ffi/src/bridge.rs
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <new>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include "gn/ffi/test_with_scope.h"
+#include "gn/label.h"
+#include "gn/output_file.h"
+#include "gn/scope.h"
+#include "gn/settings.h"
+#include "gn/source_dir.h"
+#include "gn/test_with_scope.h"
+#if __cplusplus >= 201703L
+#include <string_view>
+#endif
+
+#ifdef __GNUC__
+#pragma GCC diagnostic ignored "-Wmissing-declarations"
+#ifdef __clang__
+#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
+#endif  // __clang__
+#endif  // __GNUC__
+
+namespace rust {
+inline namespace cxxbridge1 {
+// #include "rust/cxx.h"
+
+namespace {
+template <typename T>
+class impl;
+}  // namespace
+
+class String;
+
+#ifndef CXXBRIDGE1_RUST_STR
+#define CXXBRIDGE1_RUST_STR
+class Str final {
+ public:
+  Str() noexcept;
+  Str(const String&) noexcept;
+  Str(const std::string&);
+  Str(const char*);
+  Str(const char*, std::size_t);
+#if __cplusplus >= 201703L
+  Str(std::string_view s) : Str(s.data(), s.size()) {}
+#endif
+
+  Str& operator=(const Str&) & noexcept = default;
+
+  explicit operator std::string() const;
+#if __cplusplus >= 201703L
+  explicit operator std::string_view() const;
+#endif
+
+  const char* data() const noexcept;
+  std::size_t size() const noexcept;
+  std::size_t length() const noexcept;
+  bool empty() const noexcept;
+
+  Str(const Str&) noexcept = default;
+  ~Str() noexcept = default;
+
+  using iterator = const char*;
+  using const_iterator = const char*;
+  const_iterator begin() const noexcept;
+  const_iterator end() const noexcept;
+  const_iterator cbegin() const noexcept;
+  const_iterator cend() const noexcept;
+
+  bool operator==(const Str&) const noexcept;
+  bool operator!=(const Str&) const noexcept;
+  bool operator<(const Str&) const noexcept;
+  bool operator<=(const Str&) const noexcept;
+  bool operator>(const Str&) const noexcept;
+  bool operator>=(const Str&) const noexcept;
+
+  void swap(Str&) noexcept;
+
+ private:
+  class uninit;
+  Str(uninit) noexcept;
+  friend impl<Str>;
+
+  std::array<std::uintptr_t, 2> repr;
+};
+
+#if __cplusplus >= 201703L
+inline Str cxx_to_rust(std::string_view view) {
+  return Str(view);
+}
+#endif
+
+inline Str cxx_to_rust(const std::string& s) {
+  return Str(s);
+}
+#endif  // CXXBRIDGE1_RUST_STR
+
+#ifndef CXXBRIDGE1_IS_COMPLETE
+#define CXXBRIDGE1_IS_COMPLETE
+namespace detail {
+namespace {
+template <typename T, typename = std::size_t>
+struct is_complete : std::false_type {};
+template <typename T>
+struct is_complete<T, decltype(sizeof(T))> : std::true_type {};
+}  // namespace
+}  // namespace detail
+#endif  // CXXBRIDGE1_IS_COMPLETE
+
+namespace {
+template <bool>
+struct deleter_if {
+  template <typename T>
+  void operator()(T*) {}
+};
+template <>
+struct deleter_if<true> {
+  template <typename T>
+  void operator()(T* ptr) {
+    ptr->~T();
+  }
+};
+}  // namespace
+}  // namespace cxxbridge1
+}  // namespace rust
+
+using OutputFile = ::OutputFile;
+using SourceDir = ::SourceDir;
+using Label = ::Label;
+using Settings = ::Settings;
+using Scope = ::Scope;
+using TestWithScope = ::TestWithScope;
+
+extern "C" {
+void cxxbridge1$196$OutputFile$value(::OutputFile const& self,
+                                     ::rust::Str* return$) noexcept {
+  std::string_view (::OutputFile::*value$)() const = &::OutputFile::value;
+  new (return$)::rust::Str(::rust::cxx_to_rust((self.*value$)()));
+}
+
+void cxxbridge1$196$SourceDir$SourceWithNoTrailingSlash(
+    ::SourceDir const& self,
+    ::rust::Str* return$) noexcept {
+  std::string_view (::SourceDir::*SourceWithNoTrailingSlash$)() const =
+      &::SourceDir::SourceWithNoTrailingSlash;
+  new (return$)::rust::Str(
+      ::rust::cxx_to_rust((self.*SourceWithNoTrailingSlash$)()));
+}
+
+void cxxbridge1$196$Label$dir(::Label const& self,
+                              ::SourceDir const** return$) noexcept {
+  ::SourceDir const& (::Label::*dir$)() const = &::Label::dir;
+  new (return$)::SourceDir const*(&(self.*dir$)());
+}
+
+void cxxbridge1$196$Label$name(::Label const& self,
+                               ::rust::Str* return$) noexcept {
+  const std::string& (::Label::*name$)() const = &::Label::name;
+  new (return$)::rust::Str(::rust::cxx_to_rust((self.*name$)()));
+}
+
+void cxxbridge1$196$Settings$toolchain_label(::Settings const& self,
+                                             ::Label const** return$) noexcept {
+  ::Label const& (::Settings::*toolchain_label$)() const =
+      &::Settings::toolchain_label;
+  new (return$)::Label const*(&(self.*toolchain_label$)());
+}
+
+::Settings const* cxxbridge1$196$Scope$settings_cxx(
+    ::Scope const& self) noexcept {
+  ::Settings const* (::Scope::*settings_cxx$)() const = &::Scope::settings;
+  return (self.*settings_cxx$)();
+}
+
+::TestWithScope* cxxbridge1$196$NewTestWithScope() noexcept {
+  ::std::unique_ptr<::TestWithScope> (*NewTestWithScope$)() =
+      ::NewTestWithScope;
+  return NewTestWithScope$().release();
+}
+
+::Scope* cxxbridge1$196$TestWithScope$scope_cxx(
+    ::TestWithScope& self) noexcept {
+  ::Scope* (::TestWithScope::*scope_cxx$)() = &::TestWithScope::scope;
+  return (self.*scope_cxx$)();
+}
+
+static_assert(::rust::detail::is_complete<
+                  ::std::remove_extent<::TestWithScope>::type>::value,
+              "definition of `::TestWithScope` is required");
+static_assert(sizeof(::std::unique_ptr<::TestWithScope>) == sizeof(void*), "");
+static_assert(alignof(::std::unique_ptr<::TestWithScope>) == alignof(void*),
+              "");
+void cxxbridge1$unique_ptr$TestWithScope$null(
+    ::std::unique_ptr<::TestWithScope>* ptr) noexcept {
+  ::new (ptr)::std::unique_ptr<::TestWithScope>();
+}
+void cxxbridge1$unique_ptr$TestWithScope$raw(
+    ::std::unique_ptr<::TestWithScope>* ptr,
+    ::std::unique_ptr<::TestWithScope>::pointer raw) noexcept {
+  ::new (ptr)::std::unique_ptr<::TestWithScope>(raw);
+}
+::std::unique_ptr<::TestWithScope>::element_type const*
+cxxbridge1$unique_ptr$TestWithScope$get(
+    ::std::unique_ptr<::TestWithScope> const& ptr) noexcept {
+  return ptr.get();
+}
+::std::unique_ptr<::TestWithScope>::pointer
+cxxbridge1$unique_ptr$TestWithScope$release(
+    ::std::unique_ptr<::TestWithScope>& ptr) noexcept {
+  return ptr.release();
+}
+void cxxbridge1$unique_ptr$TestWithScope$drop(
+    ::std::unique_ptr<::TestWithScope>* ptr) noexcept {
+  ::rust::deleter_if<::rust::detail::is_complete<::TestWithScope>::value>{}(
+      ptr);
+}
+}  // extern "C"
diff --git a/src/gn/ffi/bridge.h b/src/gn/ffi/bridge.h
new file mode 100644
index 0000000..d56352b
--- /dev/null
+++ b/src/gn/ffi/bridge.h
@@ -0,0 +1,100 @@
+// This file is generated by src/gn/ffi/update_bridge.sh. Do not edit manually.
+// Source: src/gn/ffi/starlark/crates/ffi/src/bridge.rs
+#pragma once
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include "gn/ffi/test_with_scope.h"
+#include "gn/label.h"
+#include "gn/output_file.h"
+#include "gn/scope.h"
+#include "gn/settings.h"
+#include "gn/source_dir.h"
+#include "gn/test_with_scope.h"
+#if __cplusplus >= 201703L
+#include <string_view>
+#endif
+
+namespace rust {
+inline namespace cxxbridge1 {
+// #include "rust/cxx.h"
+
+namespace {
+template <typename T>
+class impl;
+}  // namespace
+
+class String;
+
+#ifndef CXXBRIDGE1_RUST_STR
+#define CXXBRIDGE1_RUST_STR
+class Str final {
+ public:
+  Str() noexcept;
+  Str(const String&) noexcept;
+  Str(const std::string&);
+  Str(const char*);
+  Str(const char*, std::size_t);
+#if __cplusplus >= 201703L
+  Str(std::string_view s) : Str(s.data(), s.size()) {}
+#endif
+
+  Str& operator=(const Str&) & noexcept = default;
+
+  explicit operator std::string() const;
+#if __cplusplus >= 201703L
+  explicit operator std::string_view() const;
+#endif
+
+  const char* data() const noexcept;
+  std::size_t size() const noexcept;
+  std::size_t length() const noexcept;
+  bool empty() const noexcept;
+
+  Str(const Str&) noexcept = default;
+  ~Str() noexcept = default;
+
+  using iterator = const char*;
+  using const_iterator = const char*;
+  const_iterator begin() const noexcept;
+  const_iterator end() const noexcept;
+  const_iterator cbegin() const noexcept;
+  const_iterator cend() const noexcept;
+
+  bool operator==(const Str&) const noexcept;
+  bool operator!=(const Str&) const noexcept;
+  bool operator<(const Str&) const noexcept;
+  bool operator<=(const Str&) const noexcept;
+  bool operator>(const Str&) const noexcept;
+  bool operator>=(const Str&) const noexcept;
+
+  void swap(Str&) noexcept;
+
+ private:
+  class uninit;
+  Str(uninit) noexcept;
+  friend impl<Str>;
+
+  std::array<std::uintptr_t, 2> repr;
+};
+
+#if __cplusplus >= 201703L
+inline Str cxx_to_rust(std::string_view view) {
+  return Str(view);
+}
+#endif
+
+inline Str cxx_to_rust(const std::string& s) {
+  return Str(s);
+}
+#endif  // CXXBRIDGE1_RUST_STR
+}  // namespace cxxbridge1
+}  // namespace rust
+
+using OutputFile = ::OutputFile;
+using SourceDir = ::SourceDir;
+using Label = ::Label;
+using Settings = ::Settings;
+using Scope = ::Scope;
+using TestWithScope = ::TestWithScope;
diff --git a/src/gn/ffi/label.cc b/src/gn/ffi/label.cc
deleted file mode 100644
index 3818d4c..0000000
--- a/src/gn/ffi/label.cc
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2026 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "gn/label.h"
-#include "cxx.h"
-
-extern "C" {
-
-const SourceDir& GetLabelDir(const Label& label) {
-  return label.dir();
-}
-
-rust::Str GetLabelName(const Label& label) {
-  return label.name();
-}
-
-rust::Str GetSourceDirValue(const SourceDir& dir) {
-  return dir.SourceWithNoTrailingSlash();
-}
-
-}  // extern "C"
diff --git a/src/gn/ffi/output_file.cc b/src/gn/ffi/output_file.cc
deleted file mode 100644
index 5437eba..0000000
--- a/src/gn/ffi/output_file.cc
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2026 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "gn/output_file.h"
-
-#include "cxx.h"
-
-extern "C" {
-
-rust::Str GetOutputFilePath(const OutputFile& file) {
-  return file.value();
-}
-
-}  // extern "C"
diff --git a/src/gn/ffi/settings.cc b/src/gn/ffi/settings.cc
deleted file mode 100644
index f0e688b..0000000
--- a/src/gn/ffi/settings.cc
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2026 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "gn/settings.h"
-
-#include "gn/label.h"
-
-extern "C" {
-
-const Label& GetToolchainLabelFromSettings(const Settings& settings) {
-  return settings.toolchain_label();
-}
-
-}  // extern "C"
diff --git a/src/gn/ffi/test_with_scope.h b/src/gn/ffi/test_with_scope.h
new file mode 100644
index 0000000..d101ead
--- /dev/null
+++ b/src/gn/ffi/test_with_scope.h
@@ -0,0 +1,16 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_FFI_TEST_WITH_SCOPE_H_
+#define TOOLS_GN_FFI_TEST_WITH_SCOPE_H_
+
+#include <memory>
+
+#include "gn/test_with_scope.h"
+
+inline std::unique_ptr<TestWithScope> NewTestWithScope() {
+  return std::make_unique<TestWithScope>();
+}
+
+#endif  // TOOLS_GN_FFI_TEST_WITH_SCOPE_H_
diff --git a/src/gn/ffi/update_bridge.sh b/src/gn/ffi/update_bridge.sh
new file mode 100755
index 0000000..818fd10
--- /dev/null
+++ b/src/gn/ffi/update_bridge.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copyright 2026 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+set -euo pipefail
+
+# Resolve the directory containing this script.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+STARLARK_DIR="${SCRIPT_DIR}/../starlark"
+LOCAL_BIN_DIR="${SCRIPT_DIR}/.bin"
+
+if [ ! -f "${LOCAL_BIN_DIR}/bin/cxxbridge" ]; then
+  echo "Fetching cargo dependencies to ensure cxx is in cache..."
+  cargo fetch --manifest-path "${STARLARK_DIR}/Cargo.toml"
+
+  echo "Locating cxxbridge source in cargo cache..."
+  CMD_DIR=$(find "${HOME}/.cargo/git/checkouts" -type d -path "*/cxx-*/bridge/cmd" 2>/dev/null | head -n 1)
+
+  if [ -z "${CMD_DIR}" ]; then
+    echo "Error: Could not find cxx checkout under ~/.cargo/git/checkouts/" >&2
+    exit 1
+  fi
+
+  echo "Compiling cxxbridge tool from cargo cache..."
+  cargo install --path "${CMD_DIR}" --root "${LOCAL_BIN_DIR}"
+fi
+
+(
+  echo "// This file is generated by src/gn/ffi/update_bridge.sh. Do not edit manually."
+  echo "// Source: src/gn/ffi/starlark/crates/ffi/src/bridge.rs"
+  "${LOCAL_BIN_DIR}/bin/cxxbridge" "${SCRIPT_DIR}/../starlark/crates/ffi/src/bridge.rs" --header
+) > "${SCRIPT_DIR}/bridge.h"
+
+(
+  echo "// This file is generated by src/gn/ffi/update_bridge.sh. Do not edit manually."
+  echo "// Source: src/gn/ffi/starlark/crates/ffi/src/bridge.rs"
+  "${LOCAL_BIN_DIR}/bin/cxxbridge" "${SCRIPT_DIR}/../starlark/crates/ffi/src/bridge.rs"
+) > "${SCRIPT_DIR}/bridge.cc"
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock
index 0444f96..dc75e26 100644
--- a/src/gn/starlark/Cargo.lock
+++ b/src/gn/starlark/Cargo.lock
@@ -380,9 +380,8 @@
 
 [[package]]
 name = "cxx"
-version = "1.0.194"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
+version = "1.0.196"
+source = "git+https://github.com/matts1/cxx.git?branch=gn#a3259e72e27a86b6d722bd9d168a2c29c96c898f"
 dependencies = [
  "cc",
  "cxx-build",
@@ -395,9 +394,8 @@
 
 [[package]]
 name = "cxx-build"
-version = "1.0.194"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
+version = "1.0.196"
+source = "git+https://github.com/matts1/cxx.git?branch=gn#a3259e72e27a86b6d722bd9d168a2c29c96c898f"
 dependencies = [
  "cc",
  "codespan-reporting",
@@ -410,9 +408,8 @@
 
 [[package]]
 name = "cxxbridge-cmd"
-version = "1.0.194"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
+version = "1.0.196"
+source = "git+https://github.com/matts1/cxx.git?branch=gn#a3259e72e27a86b6d722bd9d168a2c29c96c898f"
 dependencies = [
  "clap",
  "codespan-reporting",
@@ -424,15 +421,13 @@
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.194"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
+version = "1.0.196"
+source = "git+https://github.com/matts1/cxx.git?branch=gn#a3259e72e27a86b6d722bd9d168a2c29c96c898f"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.194"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
+version = "1.0.196"
+source = "git+https://github.com/matts1/cxx.git?branch=gn#a3259e72e27a86b6d722bd9d168a2c29c96c898f"
 dependencies = [
  "indexmap",
  "proc-macro2",
@@ -613,7 +608,7 @@
 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
 dependencies = [
  "libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -641,7 +636,7 @@
 dependencies = [
  "cfg-if",
  "rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1238,7 +1233,7 @@
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1716,7 +1711,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1736,15 +1731,6 @@
 
 [[package]]
 name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-sys"
 version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml
index dc9b615..45a0c66 100644
--- a/src/gn/starlark/Cargo.toml
+++ b/src/gn/starlark/Cargo.toml
@@ -33,6 +33,8 @@
 # create records directly in rust (could only create them by running in starlark
 # previously).
 [patch.crates-io]
+cxx = { git = "https://github.com/matts1/cxx.git", branch = "gn" }
+cxxbridge-macro = { git = "https://github.com/matts1/cxx.git", branch = "gn" }
 starlark = { git = "https://github.com/matts1/starlark-rust.git", branch = "gn_starlark" }
 starlark_derive = { git = "https://github.com/matts1/starlark-rust.git", branch = "gn_starlark" }
 starlark_map = { git = "https://github.com/matts1/starlark-rust.git", branch = "gn_starlark" }
diff --git a/src/gn/starlark/crates/ffi/Cargo.toml b/src/gn/starlark/crates/ffi/Cargo.toml
index 11ab908..c740bf3 100644
--- a/src/gn/starlark/crates/ffi/Cargo.toml
+++ b/src/gn/starlark/crates/ffi/Cargo.toml
@@ -7,7 +7,6 @@
 workspace = true
 
 [lib]
-test = false
 doctest = false
 
 [dependencies]
diff --git a/src/gn/starlark/crates/ffi/src/bridge.rs b/src/gn/starlark/crates/ffi/src/bridge.rs
new file mode 100644
index 0000000..8898d2a
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/bridge.rs
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The consolidated cxx FFI bridge defining all shared C++ classes, structs,
+/// methods, and constructors utilized by the high-level Rust wrappers.
+///
+/// This file does several things:
+/// * It generates types usable by rust.
+/// * The `cxxbridge --header` command can be ran to re-generate the C++
+///   headers.
+///   * This allows for C++ code to #include rust types
+/// * The `cxxbridge` command generates shims to allow us to use C++ types in
+///   rust.
+#[cxx::bridge]
+// CxxBridge requires a module, but we don't want one. So we make a private one
+// and re-export all fields.
+mod dummy {
+    unsafe extern "C++" {
+        // include! simply tells cxxbridge to put the #include in the generated C++
+        // source code. It does not do anything on the rust side.
+        include!("gn/ffi/test_with_scope.h");
+        include!("gn/label.h");
+        include!("gn/output_file.h");
+        include!("gn/scope.h");
+        include!("gn/settings.h");
+        include!("gn/source_dir.h");
+        include!("gn/test_with_scope.h");
+
+        type OutputFile;
+        #[cxx_return_type = "std::string_view"]
+        pub(in crate::output_file) fn value(self: &OutputFile) -> &str;
+
+        type SourceDir;
+        #[cxx_return_type = "std::string_view"]
+        pub(in crate::label) fn SourceWithNoTrailingSlash(self: &SourceDir) -> &str;
+
+        type Label;
+        pub(in crate::label) fn dir(self: &Label) -> &SourceDir;
+        #[cxx_return_type = "const std::string&"]
+        pub fn name(self: &Label) -> &str;
+
+        type Settings;
+        pub(in crate::settings) fn toolchain_label(self: &Settings) -> &Label;
+
+        type Scope;
+        #[rust_name = "settings_cxx"]
+        pub(crate) fn settings(self: &Scope) -> *const Settings;
+
+        type TestWithScope;
+        pub(in crate::test_with_scope) fn NewTestWithScope() -> UniquePtr<TestWithScope>;
+        #[rust_name = "scope_cxx"]
+        pub(in crate::test_with_scope) fn scope(self: Pin<&mut TestWithScope>) -> *mut Scope;
+    }
+}
+
+pub use dummy::*;
diff --git a/src/gn/starlark/crates/ffi/src/label.rs b/src/gn/starlark/crates/ffi/src/label.rs
index df3c42b..5603772 100644
--- a/src/gn/starlark/crates/ffi/src/label.rs
+++ b/src/gn/starlark/crates/ffi/src/label.rs
@@ -4,26 +4,12 @@
 
 use types::{LabelRef, PackageRef};
 
-use crate::declare_opaque_type;
-declare_opaque_type!(pub Label);
+use crate::{Label, SourceDir};
 
 impl Label {
     /// Returns the directory part of the label (the package path).
     pub fn package(&self) -> &PackageRef {
-        extern "C" {
-            fn GetLabelDir(label: &Label) -> &SourceDir;
-        }
-        // Safety: Just an FFI function.
-        unsafe { GetLabelDir(self) }.as_rust()
-    }
-
-    /// Returns the name part of the label.
-    pub fn name(&self) -> &str {
-        extern "C" {
-            fn GetLabelName(label: &Label) -> &str;
-        }
-        // Safety: Just an FFI function.
-        unsafe { GetLabelName(self) }
+        self.dir().as_rust().unwrap()
     }
 
     /// Returns a `LabelRef` referencing the directory and name of this label.
@@ -32,21 +18,24 @@
     }
 }
 
-declare_opaque_type!(pub SourceDir);
-
 impl SourceDir {
-    pub fn as_rust(&self) -> &types::PackageRef {
-        extern "C" {
-            fn GetSourceDirValue(dir: &SourceDir) -> &str;
-        }
-        // Safety: Just an FFI function.
-        let s = unsafe { GetSourceDirValue(self) };
-        // While source dirs aren't guarunteed to start with "//" (they may be
-        // absolute), we only convert source dirs to rust for either labels or
-        // BUILD.gn directories, both of which are guarunteed to be
-        // source-relative.
-        debug_assert!(s.starts_with("//"));
-        // Safety: Guarunteed to be a valid package.
-        unsafe { types::PackageRef::new_unchecked(s) }
+    pub fn as_rust(&self) -> starlark::Result<&types::PackageRef> {
+        PackageRef::new(self.SourceWithNoTrailingSlash())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use types::{LabelRef, PackageRef};
+
+    use crate::TestWithScope;
+
+    #[test]
+    fn test_label() {
+        let mut setup = TestWithScope::new();
+        assert_eq!(
+            setup.scope().settings().toolchain(),
+            LabelRef::new(PackageRef::new("//toolchain").unwrap(), "default")
+        );
     }
 }
diff --git a/src/gn/starlark/crates/ffi/src/lib.rs b/src/gn/starlark/crates/ffi/src/lib.rs
index cfb5f72..d9a9fda 100644
--- a/src/gn/starlark/crates/ffi/src/lib.rs
+++ b/src/gn/starlark/crates/ffi/src/lib.rs
@@ -2,72 +2,26 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-//! Low-level FFI bindings and opaque types for interoperating between the C++
-//! GN codebase and the Rust Starlark interpreter crates.
+//! Low-level FFI bindings and types for interoperating between the C++
+//! GN codebase and the Rust Starlark interpreter crates using `cxx`.
 //!
-//! **Type Equivalence**
+//! **Architecture & Bridge design**
 //!
-//! WARNING: We have no way of checking that the C++ type signatures match the
-//! rust type signatures. This means that if you change the signature of a
-//! function on one side, you *might* get a link error, or might just get UB.
+//! Rather than manually compiling raw `extern "C"` FFI wrappers, all FFI
+//! boundary mappings are consolidated within `bridge.rs` under a single
+//! `#[cxx::bridge]` module.
 //!
-//! As long as C++ and rust see a type identically, we don't care how it works
-//! under the hood.
-//! * Most custom types will need to be passed by reference
-//!   * C++ &T is just a non-null pointer.
-//!   * Rust references are just non-null pointers
-//!   * Rust Option<&T> are pointers
-//! * If you define a struct in C++ and a #[repr(C)] one in rust, you can pass
-//!   by value instead of by reference.
-//!   * cxx.h does this for some rust types in C++ (rust::*, eg. rust::Str =
-//!     &str)
-//!   * cxx.h does this for some C++ types in rust (cxx::*, eg.
-//!     cxx::CxxVector<T> = std::vector<T>)
+//! This module is then transpiled with cxxbridge into a C++ header and source
+//! file.
 //!
-//! **Exposing rust functions to C++**
-//!
-//! Declare a function `#[no_mangle] pub extern "C"` in rust
-//! ```rust
-//! // foo.rs
-//! #[no_mangle]
-//! pub extern "C" fn my_function(foo: &Foo) -> &str {
-//!     foo.name()
-//! }
-//! ```
-//!
-//! Declare the C++ function for this in a header file.
-//! A clean C++ API has not yet been implemented.
-//!
-//! **Exposing C++ functions to rust**
-//!
-//! Define an `extern "C"` function in C++. Do not define a header file, as it
-//! should not be included from C++ code.
-//! ```cpp
-//! extern "C" rust::Str GetLabelName(const Label& label) {
-//!   return rust::Str(label.name());
-//! }
-//! ```
-//!
-//! Declare the equivalent extern "C" function in rust. Do so inside a function
-//! definition, as the canonical way to access this function *safely*.
-//! For example:
-//! ```rust
-//! declare_opaque_type!(Label);
-//! impl Label {
-//!     fn name(&self) -> &str {
-//!         extern "C" {
-//!             fn GetLabelName(label: &Label) -> &str;
-//!         }
-//!         unsafe { GetLabelName(self) }
-//!     }
-//! }
-//! ```
+//! Safe APIs for C++ types are then exposed in the impl functions for each of
+//! these types in their own files.
+mod bridge;
+mod label;
+mod output_file;
+mod scope;
+mod settings;
+mod test_with_scope;
 
-pub mod label;
-pub mod opaque;
-pub mod output_file;
-pub mod settings;
-
-pub use label::{Label, SourceDir};
-pub use output_file::OutputFile;
-pub use settings::Settings;
+pub use bridge::{Label, OutputFile, Scope, Settings, SourceDir};
+pub use test_with_scope::TestWithScope;
diff --git a/src/gn/starlark/crates/ffi/src/opaque.rs b/src/gn/starlark/crates/ffi/src/opaque.rs
deleted file mode 100644
index 310310d..0000000
--- a/src/gn/starlark/crates/ffi/src/opaque.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2026 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/// Declares a C++ type that is opaque to rust.
-///
-/// | C++ Type            | Rust Type                    |
-/// |---------------------|------------------------------|
-/// | `const OpaqueType&` | `&'a OpaqueType`             |
-/// | `OpaqueType&`       | `&'a mut OpaqueType`         |
-/// | `const OpaqueType*` | `Option<&'a OpaqueType>`     |
-/// | `OpaqueType*`       | `Option<&'a mut OpaqueType>` |
-/// | `OpaqueType`        | **DO NOT USE**               |
-#[macro_export]
-macro_rules! declare_opaque_type {
-    ($name:ident) => {
-        $crate::declare_opaque_type!(pub $name);
-    };
-    ($vis:vis $name:ident) => {
-        #[repr(C)]
-        $vis struct $name {
-            // Private member prevents external construction or instantiation by-value.
-            // Non-zero size (1 byte) prevents Rust from optimizing references as ZSTs.
-            _private: u8,
-        }
-    };
-}
diff --git a/src/gn/starlark/crates/ffi/src/output_file.rs b/src/gn/starlark/crates/ffi/src/output_file.rs
index 60d2cff..fa05aac 100644
--- a/src/gn/starlark/crates/ffi/src/output_file.rs
+++ b/src/gn/starlark/crates/ffi/src/output_file.rs
@@ -2,20 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::declare_opaque_type;
-
-declare_opaque_type!(pub OutputFile);
+use crate::OutputFile;
 
 impl OutputFile {
     /// Converts a GN OutputFile to a starlark File.
     pub fn to_rust(&self) -> types::File {
-        extern "C" {
-            // Returned OutputFile objects will be owned by targets,
-            // which live forever, so &'static is fine.
-            fn GetOutputFilePath(file: &OutputFile) -> &'static str;
-        }
-        // Safety: Just an FFI function.
-        let s = unsafe { GetOutputFilePath(self) };
-        types::File::new(s)
+        types::File::intern(self.value())
     }
 }
diff --git a/src/gn/starlark/crates/ffi/src/scope.rs b/src/gn/starlark/crates/ffi/src/scope.rs
new file mode 100644
index 0000000..8e4c85a
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/scope.rs
@@ -0,0 +1,12 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::Scope;
+impl Scope {
+    /// Returns the settings for the given scope.
+    pub fn settings(&self) -> &crate::Settings {
+        // Safety: Settings pointer is always valid and non-null.
+        unsafe { self.settings_cxx().as_ref() }.unwrap()
+    }
+}
diff --git a/src/gn/starlark/crates/ffi/src/settings.rs b/src/gn/starlark/crates/ffi/src/settings.rs
index 2eae88e..c0f0969 100644
--- a/src/gn/starlark/crates/ffi/src/settings.rs
+++ b/src/gn/starlark/crates/ffi/src/settings.rs
@@ -2,16 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::declare_opaque_type;
-
-declare_opaque_type!(pub Settings);
+use crate::Settings;
 
 impl Settings {
     /// Returns the toolchain label for the given settings.
     pub fn toolchain(&self) -> types::LabelRef<'_> {
-        extern "C" {
-            fn GetToolchainLabelFromSettings(settings: &Settings) -> &crate::Label;
-        }
-        unsafe { GetToolchainLabelFromSettings(self).as_ref() }
+        self.toolchain_label().as_ref()
     }
 }
diff --git a/src/gn/starlark/crates/ffi/src/test_with_scope.rs b/src/gn/starlark/crates/ffi/src/test_with_scope.rs
new file mode 100644
index 0000000..b311247
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/test_with_scope.rs
@@ -0,0 +1,29 @@
+// Copyright 2026 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// A test fixture wrapper for a C++ GN TestWithScope.
+pub struct TestWithScope {
+    inner: cxx::UniquePtr<crate::bridge::TestWithScope>,
+}
+
+impl Default for TestWithScope {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl TestWithScope {
+    /// Creates a new GN TestWithScope fixture.
+    pub fn new() -> Self {
+        Self {
+            inner: crate::bridge::NewTestWithScope(),
+        }
+    }
+
+    /// Accesses the scope owned by the test fixture.
+    pub fn scope(&mut self) -> &mut crate::Scope {
+        // Safety: Scope pointer from TestWithScope is always valid and non-null.
+        unsafe { self.inner.pin_mut().scope_cxx().as_mut() }.unwrap()
+    }
+}
diff --git a/src/gn/starlark/crates/testutils/src/eval_context.rs b/src/gn/starlark/crates/testutils/src/eval_context.rs
index 0bb9752..b5707e1 100644
--- a/src/gn/starlark/crates/testutils/src/eval_context.rs
+++ b/src/gn/starlark/crates/testutils/src/eval_context.rs
@@ -44,9 +44,8 @@
     /// Creates a new `FakeEvalContext` for a given package name.
     pub fn new(package: &str) -> Self {
         let session = FakeSession::new();
-        let package_ref = PackageRef::new_for_testing(package).to_owned();
         Self {
-            package: package_ref,
+            package: PackageRef::new(package).unwrap().to_owned(),
             current_toolchain: session.default_toolchain.clone(),
             session,
             path_resolver: PathResolver::new_for_testing(),