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(),