Create a simple session type and create a test for it.

It's mostly filled with todo! for implementing the more complex logic,
but it can load values from starlark files.

Bug: 528225104
Change-Id: I72fab7d0c2691453b612f1beb39f86d16a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24580
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Reviewed-by: Richard Wang <richardwa@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/build/gen.py b/build/gen.py
index eacd28a..c7d03f7 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -764,6 +764,7 @@
               'src/gn/ffi/bridge.cc',
               'src/gn/ffi/scope.cc',
               'src/gn/ffi/value.cc',
+              'src/gn/ffi/session.cc',
               'src/gn/filesystem_utils.cc',
               'src/gn/file_writer.cc',
               'src/gn/frameworks_utils.cc',
@@ -966,6 +967,7 @@
         'src/gn/path_output_unittest.cc',
         'src/gn/pattern_unittest.cc',
         'src/gn/pointer_set_unittest.cc',
+        'src/gn/range_utils_unittest.cc',
         'src/gn/resolved_target_data_unittest.cc',
         'src/gn/resolved_target_deps_unittest.cc',
         'src/gn/runtime_deps_unittest.cc',
@@ -1059,6 +1061,12 @@
   executables['gn']['libs'].extend(static_libraries.keys())
   executables['gn_unittests']['libs'].extend(static_libraries.keys())
 
+  if options.starlark:
+    executables['gn_unittests']['sources'].extend([
+        'src/gn/ffi/session_unittest.cc',
+    ])
+    executables['gn_unittests']['libs'].append('gn_starlark')
+
   # Write the absolute path of the source root to a file in the output directory
   # so that tests can locate the source tree robustly.
   with open(os.path.join(options.out_path, 'source_root.txt'), 'w') as f:
diff --git a/src/gn/ffi/bridge.cc b/src/gn/ffi/bridge.cc
index 5b94043..912855c 100644
--- a/src/gn/ffi/bridge.cc
+++ b/src/gn/ffi/bridge.cc
@@ -32,6 +32,7 @@
 
 #ifdef __GNUC__
 #pragma GCC diagnostic ignored "-Wmissing-declarations"
+#pragma GCC diagnostic ignored "-Wshadow"
 #ifdef __clang__
 #pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
 #endif // __clang__
@@ -54,8 +55,6 @@
 class impl;
 } // namespace
 
-class Opaque;
-
 template <typename T>
 ::std::size_t size_of();
 template <typename T>
@@ -504,6 +503,172 @@
 }
 #endif // CXXBRIDGE1_RUST_SLICE
 
+#ifndef CXXBRIDGE1_RUST_BOX
+#define CXXBRIDGE1_RUST_BOX
+template <typename T>
+class Box final {
+public:
+  using element_type = T;
+  using const_pointer =
+      typename std::add_pointer<typename std::add_const<T>::type>::type;
+  using pointer = typename std::add_pointer<T>::type;
+
+  Box() = delete;
+  Box(Box &&) noexcept;
+  ~Box() noexcept;
+
+  explicit Box(const T &);
+  explicit Box(T &&);
+
+  Box &operator=(Box &&) & noexcept;
+
+  const T *operator->() const noexcept;
+  const T &operator*() const noexcept;
+  T *operator->() noexcept;
+  T &operator*() noexcept;
+
+  template <typename... Fields>
+  static Box in_place(Fields &&...);
+
+  void swap(Box &) noexcept;
+
+  static Box from_raw(T *) noexcept;
+
+  T *into_raw() noexcept;
+
+  /* Deprecated */ using value_type = element_type;
+
+private:
+  class uninit;
+  class allocation;
+  Box(uninit) noexcept;
+  void drop() noexcept;
+
+  friend void swap(Box &lhs, Box &rhs) noexcept { lhs.swap(rhs); }
+
+  T *ptr;
+};
+
+template <typename T>
+class Box<T>::uninit {};
+
+template <typename T>
+class Box<T>::allocation {
+  static T *alloc() noexcept;
+  static void dealloc(T *) noexcept;
+
+public:
+  allocation() noexcept : ptr(alloc()) {}
+  ~allocation() noexcept {
+    if (this->ptr) {
+      dealloc(this->ptr);
+    }
+  }
+  T *ptr;
+};
+
+template <typename T>
+Box<T>::Box(Box &&other) noexcept : ptr(other.ptr) {
+  other.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::Box(const T &val) {
+  allocation alloc;
+  ::new (alloc.ptr) T(val);
+  this->ptr = alloc.ptr;
+  alloc.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::Box(T &&val) {
+  allocation alloc;
+  ::new (alloc.ptr) T(std::move(val));
+  this->ptr = alloc.ptr;
+  alloc.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::~Box() noexcept {
+  if (this->ptr) {
+    this->drop();
+  }
+}
+
+template <typename T>
+Box<T> &Box<T>::operator=(Box &&other) & noexcept {
+  if (this->ptr) {
+    this->drop();
+  }
+  this->ptr = other.ptr;
+  other.ptr = nullptr;
+  return *this;
+}
+
+template <typename T>
+const T *Box<T>::operator->() const noexcept {
+  return this->ptr;
+}
+
+template <typename T>
+const T &Box<T>::operator*() const noexcept {
+  return *this->ptr;
+}
+
+template <typename T>
+T *Box<T>::operator->() noexcept {
+  return this->ptr;
+}
+
+template <typename T>
+T &Box<T>::operator*() noexcept {
+  return *this->ptr;
+}
+
+template <typename T>
+template <typename... Fields>
+Box<T> Box<T>::in_place(Fields &&...fields) {
+  allocation alloc;
+  auto ptr = alloc.ptr;
+  ::new (ptr) T{std::forward<Fields>(fields)...};
+  alloc.ptr = nullptr;
+  return from_raw(ptr);
+}
+
+template <typename T>
+void Box<T>::swap(Box &rhs) noexcept {
+  using std::swap;
+  swap(this->ptr, rhs.ptr);
+}
+
+template <typename T>
+Box<T> Box<T>::from_raw(T *raw) noexcept {
+  Box box = uninit{};
+  box.ptr = raw;
+  return box;
+}
+
+template <typename T>
+T *Box<T>::into_raw() noexcept {
+  T *raw = this->ptr;
+  this->ptr = nullptr;
+  return raw;
+}
+
+template <typename T>
+Box<T>::Box(uninit) noexcept {}
+#endif // CXXBRIDGE1_RUST_BOX
+
+#ifndef CXXBRIDGE1_RUST_OPAQUE
+#define CXXBRIDGE1_RUST_OPAQUE
+class Opaque {
+public:
+  Opaque() = delete;
+  Opaque(const Opaque &) = delete;
+  ~Opaque() = delete;
+};
+#endif // CXXBRIDGE1_RUST_OPAQUE
+
 #ifndef CXXBRIDGE1_IS_COMPLETE
 #define CXXBRIDGE1_IS_COMPLETE
 namespace detail {
@@ -572,6 +737,13 @@
 }
 #endif // CXXBRIDGE1_LAYOUT
 
+template <typename T>
+union ManuallyDrop {
+  T value;
+  ManuallyDrop(T &&value) : value(::std::move(value)) {}
+  ~ManuallyDrop() {}
+};
+
 namespace {
 template <bool> struct deleter_if {
   template <typename T> void operator()(T *) {}
@@ -604,6 +776,7 @@
 using TestWithScope = ::TestWithScope;
 using Value = ::Value;
 using ParseNode = ::ParseNode;
+struct Session;
 
 #ifndef CXXBRIDGE1_STRUCT_Any
 #define CXXBRIDGE1_STRUCT_Any
@@ -655,6 +828,23 @@
 };
 #endif // CXXBRIDGE1_ENUM_ValueType
 
+#ifndef CXXBRIDGE1_STRUCT_Session
+#define CXXBRIDGE1_STRUCT_Session
+struct Session final : public ::rust::Opaque {
+  static ::rust::Box<::Session> new_cxx(::rust::Str source_root, ::rust::Str source_root_rel) noexcept;
+  static ::rust::Box<::Session> new_for_testing() noexcept;
+  void load_values(::rust::Str label, ::rust::Str relative_to, ::rust::Slice<::rust::Str const> keys, ::Scope &scope, ::Settings const &settings, ::ParseNodePtr origin, ::Err &err) const noexcept;
+  ~Session() = delete;
+
+private:
+  friend ::rust::layout;
+  struct layout {
+    static ::std::size_t size() noexcept;
+    static ::std::size_t align() noexcept;
+  };
+};
+#endif // CXXBRIDGE1_STRUCT_Session
+
 extern "C" {
 bool cxxbridge1$196$Err$has_error(::Err const &self) noexcept {
   bool (::Err::*has_error$)() const = &::Err::has_error;
@@ -736,6 +926,11 @@
   return GetValue$(scope, ident);
 }
 
+::Value *cxxbridge1$196$SetValue(::Scope &scope, ::rust::Str ident, ::ParseNodePtr *origin) noexcept {
+  ::Value &(*SetValue$)(::Scope &, ::rust::Str, ::ParseNodePtr) = ::SetValue;
+  return &SetValue$(scope, ident, ::std::move(*origin));
+}
+
 ::Settings const *cxxbridge1$196$Scope$settings_cxx(::Scope const &self) noexcept {
   ::Settings const *(::Scope::*settings_cxx$)() const = &::Scope::settings;
   return (self.*settings_cxx$)();
@@ -820,7 +1015,38 @@
   void (*SetValueScope$)(::Value &, ::ParseNodePtr, ::std::unique_ptr<::Scope>) = ::SetValueScope;
   SetValueScope$(val, ::std::move(*origin), ::std::unique_ptr<::Scope>(scope));
 }
+::std::size_t cxxbridge1$196$Session$operator$sizeof() noexcept;
+::std::size_t cxxbridge1$196$Session$operator$alignof() noexcept;
 
+::Session *cxxbridge1$196$Session$new(::rust::Str source_root, ::rust::Str source_root_rel) noexcept;
+
+::Session *cxxbridge1$196$Session$new_for_testing() noexcept;
+
+void cxxbridge1$196$Session$load_values(::Session const &self, ::rust::Str label, ::rust::Str relative_to, ::rust::Slice<::rust::Str const> keys, ::Scope &scope, ::Settings const &settings, ::ParseNodePtr *origin, ::Err &err) noexcept;
+} // extern "C"
+
+::std::size_t Session::layout::size() noexcept {
+  return cxxbridge1$196$Session$operator$sizeof();
+}
+
+::std::size_t Session::layout::align() noexcept {
+  return cxxbridge1$196$Session$operator$alignof();
+}
+
+::rust::Box<::Session> Session::new_cxx(::rust::Str source_root, ::rust::Str source_root_rel) noexcept {
+  return ::rust::Box<::Session>::from_raw(cxxbridge1$196$Session$new(source_root, source_root_rel));
+}
+
+::rust::Box<::Session> Session::new_for_testing() noexcept {
+  return ::rust::Box<::Session>::from_raw(cxxbridge1$196$Session$new_for_testing());
+}
+
+void Session::load_values(::rust::Str label, ::rust::Str relative_to, ::rust::Slice<::rust::Str const> keys, ::Scope &scope, ::Settings const &settings, ::ParseNodePtr origin, ::Err &err) const noexcept {
+  ::rust::ManuallyDrop<::ParseNodePtr> origin$(::std::move(origin));
+  cxxbridge1$196$Session$load_values(*this, label, relative_to, keys, scope, settings, &origin$.value, err);
+}
+
+extern "C" {
 static_assert(::rust::detail::is_complete<::std::remove_extent<::Err>::type>::value, "definition of `::Err` is required");
 static_assert(sizeof(::std::unique_ptr<::Err>) == sizeof(void *), "");
 static_assert(alignof(::std::unique_ptr<::Err>) == alignof(void *), "");
@@ -896,4 +1122,25 @@
 void cxxbridge1$unique_ptr$Value$drop(::std::unique_ptr<::Value> *ptr) noexcept {
   ::rust::deleter_if<::rust::detail::is_complete<::Value>::value>{}(ptr);
 }
+
+::Session *cxxbridge1$box$Session$alloc() noexcept;
+void cxxbridge1$box$Session$dealloc(::Session *) noexcept;
+void cxxbridge1$box$Session$drop(::rust::Box<::Session> *ptr) noexcept;
 } // extern "C"
+
+namespace rust {
+inline namespace cxxbridge1 {
+template <>
+::Session *Box<::Session>::allocation::alloc() noexcept {
+  return cxxbridge1$box$Session$alloc();
+}
+template <>
+void Box<::Session>::allocation::dealloc(::Session *ptr) noexcept {
+  cxxbridge1$box$Session$dealloc(ptr);
+}
+template <>
+void Box<::Session>::drop() noexcept {
+  cxxbridge1$box$Session$drop(this);
+}
+} // namespace cxxbridge1
+} // namespace rust
diff --git a/src/gn/ffi/bridge.h b/src/gn/ffi/bridge.h
index e4baff8..6435cb1 100644
--- a/src/gn/ffi/bridge.h
+++ b/src/gn/ffi/bridge.h
@@ -19,9 +19,11 @@
 #include <cstdint>
 #include <iterator>
 #include <memory>
+#include <new>
 #include <stdexcept>
 #include <string>
 #include <type_traits>
+#include <utility>
 #if __cplusplus >= 201703L
 #include <string_view>
 #endif
@@ -46,8 +48,6 @@
 class impl;
 } // namespace
 
-class Opaque;
-
 template <typename T>
 ::std::size_t size_of();
 template <typename T>
@@ -496,6 +496,172 @@
 }
 #endif // CXXBRIDGE1_RUST_SLICE
 
+#ifndef CXXBRIDGE1_RUST_BOX
+#define CXXBRIDGE1_RUST_BOX
+template <typename T>
+class Box final {
+public:
+  using element_type = T;
+  using const_pointer =
+      typename std::add_pointer<typename std::add_const<T>::type>::type;
+  using pointer = typename std::add_pointer<T>::type;
+
+  Box() = delete;
+  Box(Box &&) noexcept;
+  ~Box() noexcept;
+
+  explicit Box(const T &);
+  explicit Box(T &&);
+
+  Box &operator=(Box &&) & noexcept;
+
+  const T *operator->() const noexcept;
+  const T &operator*() const noexcept;
+  T *operator->() noexcept;
+  T &operator*() noexcept;
+
+  template <typename... Fields>
+  static Box in_place(Fields &&...);
+
+  void swap(Box &) noexcept;
+
+  static Box from_raw(T *) noexcept;
+
+  T *into_raw() noexcept;
+
+  /* Deprecated */ using value_type = element_type;
+
+private:
+  class uninit;
+  class allocation;
+  Box(uninit) noexcept;
+  void drop() noexcept;
+
+  friend void swap(Box &lhs, Box &rhs) noexcept { lhs.swap(rhs); }
+
+  T *ptr;
+};
+
+template <typename T>
+class Box<T>::uninit {};
+
+template <typename T>
+class Box<T>::allocation {
+  static T *alloc() noexcept;
+  static void dealloc(T *) noexcept;
+
+public:
+  allocation() noexcept : ptr(alloc()) {}
+  ~allocation() noexcept {
+    if (this->ptr) {
+      dealloc(this->ptr);
+    }
+  }
+  T *ptr;
+};
+
+template <typename T>
+Box<T>::Box(Box &&other) noexcept : ptr(other.ptr) {
+  other.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::Box(const T &val) {
+  allocation alloc;
+  ::new (alloc.ptr) T(val);
+  this->ptr = alloc.ptr;
+  alloc.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::Box(T &&val) {
+  allocation alloc;
+  ::new (alloc.ptr) T(std::move(val));
+  this->ptr = alloc.ptr;
+  alloc.ptr = nullptr;
+}
+
+template <typename T>
+Box<T>::~Box() noexcept {
+  if (this->ptr) {
+    this->drop();
+  }
+}
+
+template <typename T>
+Box<T> &Box<T>::operator=(Box &&other) & noexcept {
+  if (this->ptr) {
+    this->drop();
+  }
+  this->ptr = other.ptr;
+  other.ptr = nullptr;
+  return *this;
+}
+
+template <typename T>
+const T *Box<T>::operator->() const noexcept {
+  return this->ptr;
+}
+
+template <typename T>
+const T &Box<T>::operator*() const noexcept {
+  return *this->ptr;
+}
+
+template <typename T>
+T *Box<T>::operator->() noexcept {
+  return this->ptr;
+}
+
+template <typename T>
+T &Box<T>::operator*() noexcept {
+  return *this->ptr;
+}
+
+template <typename T>
+template <typename... Fields>
+Box<T> Box<T>::in_place(Fields &&...fields) {
+  allocation alloc;
+  auto ptr = alloc.ptr;
+  ::new (ptr) T{std::forward<Fields>(fields)...};
+  alloc.ptr = nullptr;
+  return from_raw(ptr);
+}
+
+template <typename T>
+void Box<T>::swap(Box &rhs) noexcept {
+  using std::swap;
+  swap(this->ptr, rhs.ptr);
+}
+
+template <typename T>
+Box<T> Box<T>::from_raw(T *raw) noexcept {
+  Box box = uninit{};
+  box.ptr = raw;
+  return box;
+}
+
+template <typename T>
+T *Box<T>::into_raw() noexcept {
+  T *raw = this->ptr;
+  this->ptr = nullptr;
+  return raw;
+}
+
+template <typename T>
+Box<T>::Box(uninit) noexcept {}
+#endif // CXXBRIDGE1_RUST_BOX
+
+#ifndef CXXBRIDGE1_RUST_OPAQUE
+#define CXXBRIDGE1_RUST_OPAQUE
+class Opaque {
+public:
+  Opaque() = delete;
+  Opaque(const Opaque &) = delete;
+  ~Opaque() = delete;
+};
+#endif // CXXBRIDGE1_RUST_OPAQUE
+
 #ifndef CXXBRIDGE1_IS_COMPLETE
 #define CXXBRIDGE1_IS_COMPLETE
 namespace detail {
@@ -587,6 +753,7 @@
 using TestWithScope = ::TestWithScope;
 using Value = ::Value;
 using ParseNode = ::ParseNode;
+struct Session;
 
 #ifndef CXXBRIDGE1_STRUCT_Any
 #define CXXBRIDGE1_STRUCT_Any
@@ -637,3 +804,20 @@
   Scope = 5,
 };
 #endif // CXXBRIDGE1_ENUM_ValueType
+
+#ifndef CXXBRIDGE1_STRUCT_Session
+#define CXXBRIDGE1_STRUCT_Session
+struct Session final : public ::rust::Opaque {
+  static ::rust::Box<::Session> new_cxx(::rust::Str source_root, ::rust::Str source_root_rel) noexcept;
+  static ::rust::Box<::Session> new_for_testing() noexcept;
+  void load_values(::rust::Str label, ::rust::Str relative_to, ::rust::Slice<::rust::Str const> keys, ::Scope &scope, ::Settings const &settings, ::ParseNodePtr origin, ::Err &err) const noexcept;
+  ~Session() = delete;
+
+private:
+  friend ::rust::layout;
+  struct layout {
+    static ::std::size_t size() noexcept;
+    static ::std::size_t align() noexcept;
+  };
+};
+#endif // CXXBRIDGE1_STRUCT_Session
diff --git a/src/gn/ffi/scope.cc b/src/gn/ffi/scope.cc
index 6b3ed4d..1816803 100644
--- a/src/gn/ffi/scope.cc
+++ b/src/gn/ffi/scope.cc
@@ -8,6 +8,7 @@
 #include "gn/ffi/bridge.h"
 #include "gn/ffi/scope.h"
 #include "gn/ffi/slice.h"
+#include "gn/range_utils.h"
 #include "gn/scope.h"
 #include "gn/value.h"
 
@@ -71,13 +72,16 @@
 SliceAny GetScopeItems(const Scope& scope) {
   auto range =
       scope.GetCurrentScopeValues() | std::views::transform([](auto pair) {
-        return KeyValue{rust::Str(pair.first.data(), pair.first.size()),
-                        *pair.second};
+        return KeyValue{rust::Str(pair.first), *pair.second};
       });
-  return IntoSlice(std::vector<KeyValue>(range.begin(), range.end()));
+  return IntoSlice(to_vec(range));
 }
 
 const Value* GetValue(const Scope& scope, rust::Str ident) {
   std::string_view ident_sv(ident.data(), ident.size());
   return scope.GetValue(ident_sv);
 }
+
+Value& SetValue(Scope& scope, rust::Str ident, ParseNodePtr origin) {
+  return *scope.SetValue(std::string_view(ident), Value(), origin.ptr);
+}
diff --git a/src/gn/ffi/scope.h b/src/gn/ffi/scope.h
index c62af06..58a228b 100644
--- a/src/gn/ffi/scope.h
+++ b/src/gn/ffi/scope.h
@@ -9,6 +9,7 @@
 
 #include "cxx.h"
 
+struct ParseNodePtr;
 class Scope;
 class Settings;
 struct SliceAny;
@@ -44,4 +45,7 @@
 // Returns a pointer to the value in the scope or nullptr if not found.
 const Value* GetValue(const Scope& scope, rust::Str ident);
 
+// Adds a value slot to the scope under `ident` and returns a reference to it.
+Value& SetValue(Scope& scope, rust::Str ident, ParseNodePtr origin);
+
 #endif  // TOOLS_GN_FFI_SCOPE_H_
diff --git a/src/gn/ffi/session.cc b/src/gn/ffi/session.cc
new file mode 100644
index 0000000..b2329bf
--- /dev/null
+++ b/src/gn/ffi/session.cc
@@ -0,0 +1,78 @@
+// 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/ffi/session.h"
+
+#include <vector>
+
+#include "gn/build_settings.h"
+#include "gn/err.h"
+#include "gn/ffi/bridge.h"
+#include "gn/functions.h"
+#include "gn/parse_tree.h"
+#include "gn/scope.h"
+#include "gn/source_dir.h"
+#include "gn/tokenizer.h"
+
+struct Session;
+
+namespace {
+
+bool IsValidIdentifier(std::string_view str) {
+  if (str.empty())
+    return false;
+  if (!Tokenizer::IsIdentifierFirstChar(str[0]))
+    return false;
+  for (char c : str.substr(1)) {
+    if (!Tokenizer::IsIdentifierContinuingChar(c))
+      return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+bool session_load(const Session& session,
+                  const Value& label,
+                  std::span<const Value> keys,
+                  Scope& dest_scope,
+                  ParseNodePtr parse_node,
+                  Err& err) {
+  if (label.type() != Value::STRING) {
+    err = Err(label.origin(), "Invalid load path.",
+              "First argument to load must be a string corresponding to the "
+              "label for the file to load.");
+    return false;
+  }
+  std::string_view label_str = label.string_value();
+  if (!label_str.ends_with(".scl")) {
+    err = Err(label.origin(), "The file to load must be a '.scl' file.");
+    return false;
+  }
+
+  std::vector<rust::Str> keys_slice;
+  keys_slice.reserve(keys.size());
+  for (const auto& key : keys) {
+    if (key.type() != Value::STRING) {
+      err = Err(key.origin(), "Invalid variable to load.",
+                "Arguments to load must be strings.");
+      return false;
+    }
+    std::string_view val_str = key.string_value();
+    if (!IsValidIdentifier(val_str)) {
+      err = Err(key.origin(), "Invalid variable to load.",
+                "Arguments to load must be valid identifiers.");
+      return false;
+    }
+    keys_slice.push_back(rust::Str(StringAtom(val_str).str()));
+  }
+
+  session.load_values(
+      rust::Str(label.string_value()),
+      rust::Str(dest_scope.GetSourceDir().SourceWithNoTrailingSlash()),
+      rust::Slice<const rust::Str>(keys_slice), dest_scope,
+      *dest_scope.settings(), parse_node, err);
+
+  return !err.has_error();
+}
diff --git a/src/gn/ffi/session.h b/src/gn/ffi/session.h
new file mode 100644
index 0000000..75839c2
--- /dev/null
+++ b/src/gn/ffi/session.h
@@ -0,0 +1,38 @@
+// 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_SESSION_H_
+#define TOOLS_GN_FFI_SESSION_H_
+
+#include <span>
+
+class Err;
+class ParseNode;
+struct Session;
+class Scope;
+class Value;
+
+struct ParseNodePtr;
+
+// Executes the starlark file and loads the requested variables.
+// Example: session_load(
+//   session,
+//   Value(":foo.scl"),
+//   {"foo", "bar"}
+//   scope for //dir/BUILD.gn,
+//   ParseNodePtr{parse_node},
+//   err
+// )
+//
+// In this example, we execute the file //dir/foo.scl.
+// We then get the variables foo and bar and insert them into the dest_scope.
+// Unlike `import`, all imported variables *must* be used.
+bool session_load(const Session& session,
+                  const Value& label,
+                  std::span<const Value> keys,
+                  Scope& dest_scope,
+                  ParseNodePtr parse_node,
+                  Err& err);
+
+#endif  // TOOLS_GN_FFI_SESSION_H_
\ No newline at end of file
diff --git a/src/gn/ffi/session_unittest.cc b/src/gn/ffi/session_unittest.cc
new file mode 100644
index 0000000..44db44f
--- /dev/null
+++ b/src/gn/ffi/session_unittest.cc
@@ -0,0 +1,35 @@
+// 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/ffi/session.h"
+
+#include "gn/ffi/bridge.h"
+#include "gn/scope.h"
+#include "util/test/test.h"
+
+TEST(SessionTest, SessionLoad) {
+  TestWithScope setup;
+  rust::Box<Session> session = Session::new_for_testing();
+
+  std::vector<Value> keys = {Value(nullptr, "absolute_value"),
+                             Value(nullptr, "relative_value")};
+
+  setup.scope()->set_source_dir(SourceDir("//load/"));
+  Err err;
+  bool success =
+      session_load(*session, Value(nullptr, ":root.scl"), keys, *setup.scope(),
+                   ParseNodePtr{.ptr = nullptr}, err);
+  EXPECT_TRUE(success);
+  EXPECT_FALSE(err.has_error());
+
+  const Value* absolute_val = setup.scope()->GetValue("absolute_value");
+  ASSERT_TRUE(absolute_val);
+  EXPECT_EQ(absolute_val->type(), Value::STRING);
+  EXPECT_EQ(absolute_val->string_value(), "absolute");
+
+  const Value* relative_val = setup.scope()->GetValue("relative_value");
+  ASSERT_TRUE(relative_val);
+  EXPECT_EQ(relative_val->type(), Value::STRING);
+  EXPECT_EQ(relative_val->string_value(), "relative");
+}
diff --git a/src/gn/range_utils.h b/src/gn/range_utils.h
new file mode 100644
index 0000000..ef4e140
--- /dev/null
+++ b/src/gn/range_utils.h
@@ -0,0 +1,42 @@
+// 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_RANGE_UTILS_H_
+#define TOOLS_GN_RANGE_UTILS_H_
+
+#include <concepts>
+#include <ranges>
+#include <vector>
+
+template <typename R, typename T>
+concept RangeOf = std::ranges::input_range<R> &&
+                  std::convertible_to<std::ranges::range_reference_t<R>, T>;
+
+// std::ranges::to<std::vector> should be preferred, but isn't available on the
+// older versions of mac used in CI.
+template <std::ranges::input_range R>
+auto to_vec(R&& range) {
+  using ValType = std::ranges::range_value_t<R>;
+  // For common ranges (where begin() and end() return the same type, such as
+  // std::vector or simple views), we can use std::vector's iterator
+  // constructor.
+  if constexpr (std::ranges::common_range<R>) {
+    return std::vector<ValType>(range.begin(), range.end());
+  } else {
+    // For non-common ranges (where begin() and end() have different types, such
+    // as lazy split views or generators using sentinels), std::vector's
+    // iterator constructor will fail to compile. We must use a range-based for
+    // loop, which natively supports sentinel comparisons of different types.
+    std::vector<ValType> vec;
+    if constexpr (std::ranges::sized_range<R>) {
+      vec.reserve(std::ranges::size(range));
+    }
+    for (auto&& item : range) {
+      vec.push_back(std::forward<decltype(item)>(item));
+    }
+    return vec;
+  }
+}
+
+#endif  // TOOLS_GN_RANGE_UTILS_H_
diff --git a/src/gn/range_utils_unittest.cc b/src/gn/range_utils_unittest.cc
new file mode 100644
index 0000000..1437ff9
--- /dev/null
+++ b/src/gn/range_utils_unittest.cc
@@ -0,0 +1,24 @@
+// 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/range_utils.h"
+
+#include <ranges>
+#include <vector>
+
+#include "util/test/test.h"
+
+TEST(RangeUtilsTest, ToVec) {
+  // Test converting a common range (like a transformed vector) to vector.
+  std::vector<int> input = {1, 2, 3, 4, 5};
+  std::vector<int> output =
+      to_vec(input | std::views::transform([](int i) { return i + 1; }));
+  ASSERT_EQ(output, (std::vector<int>{2, 3, 4, 5, 6}));
+
+  // Test converting a non-common range (like a take_view of an infinite
+  // iota_view) to vector.
+  std::vector<int> output_non_common =
+      to_vec(std::views::take(std::views::iota(0), 5));
+  ASSERT_EQ(output_non_common, (std::vector<int>{0, 1, 2, 3, 4}));
+}
diff --git a/src/gn/starlark/Cargo.lock b/src/gn/starlark/Cargo.lock
index b4c12dc..c8e078a 100644
--- a/src/gn/starlark/Cargo.lock
+++ b/src/gn/starlark/Cargo.lock
@@ -671,7 +671,14 @@
 name = "ffi"
 version = "0.1.0"
 dependencies = [
+ "allocative",
+ "anyhow",
+ "attr",
  "cxx",
+ "depset",
+ "loader",
+ "providers",
+ "rule",
  "starlark",
  "starlark_derive",
  "thiserror",
@@ -739,6 +746,9 @@
 [[package]]
 name = "gn_starlark"
 version = "0.1.0"
+dependencies = [
+ "ffi",
+]
 
 [[package]]
 name = "hash32"
diff --git a/src/gn/starlark/Cargo.toml b/src/gn/starlark/Cargo.toml
index f534d77..c16cdfd 100644
--- a/src/gn/starlark/Cargo.toml
+++ b/src/gn/starlark/Cargo.toml
@@ -10,6 +10,9 @@
 crate-type = ["staticlib"]
 test = false
 
+[dependencies]
+ffi = { path = "crates/ffi" }
+
 [workspace]
 members = [
     ".",
diff --git a/src/gn/starlark/crates/ffi/Cargo.toml b/src/gn/starlark/crates/ffi/Cargo.toml
index c740bf3..c9e0211 100644
--- a/src/gn/starlark/crates/ffi/Cargo.toml
+++ b/src/gn/starlark/crates/ffi/Cargo.toml
@@ -10,8 +10,15 @@
 doctest = false
 
 [dependencies]
+allocative = { workspace = true }
+anyhow = { workspace = true }
+attr = { path = "../attr" }
+cxx = { workspace = true }
+depset = { path = "../depset" }
+loader = { path = "../loader" }
+providers = { path = "../providers" }
+rule = { path = "../rule" }
 starlark = { workspace = true }
 starlark_derive = { workspace = true }
-cxx = { workspace = true }
-types = { path = "../types" }
 thiserror = { workspace = true }
+types = { path = "../types" }
\ No newline at end of file
diff --git a/src/gn/starlark/crates/ffi/src/bridge.rs b/src/gn/starlark/crates/ffi/src/bridge.rs
index 396ed42..95fabcd 100644
--- a/src/gn/starlark/crates/ffi/src/bridge.rs
+++ b/src/gn/starlark/crates/ffi/src/bridge.rs
@@ -12,7 +12,12 @@
 ///   * This allows for C++ code to #include rust types
 /// * The `cxxbridge` command generates shims to allow us to use C++ types in
 ///   rust.
+use crate::session::Session;
+
 #[cxx::bridge]
+// Allow let_underscore_drop because the cxx::bridge generated code has non-binding
+// lets on C++ types with destructors.
+#[allow(let_underscore_drop)]
 // CxxBridge requires a module, but we don't want one. So we make a private one
 // and re-export all fields.
 mod dummy {
@@ -132,6 +137,11 @@
         // Returns an OwnedSlice<KeyValue> corresponding to references to each element.
         pub(in crate::scope) fn GetScopeItems(scope: &Scope) -> SliceAny;
         pub(in crate::scope) fn GetValue(scope: &Scope, ident: &str) -> *const Value;
+        pub(in crate::scope) fn SetValue<'a>(
+            scope: Pin<&'a mut Scope>,
+            ident: &str,
+            origin: ParseNodePtr,
+        ) -> Pin<&'a mut Value>;
         #[rust_name = "settings_cxx"]
         pub(in crate::scope) fn settings(self: &Scope) -> *const Settings;
 
@@ -175,6 +185,28 @@
             scope: UniquePtr<Scope>,
         );
     }
+
+    extern "Rust" {
+        type Session;
+
+        #[Self = "Session"]
+        #[cxx_name = "new_cxx"]
+        fn new(source_root: &str, source_root_rel: &str) -> Box<Session>;
+
+        #[Self = "Session"]
+        fn new_for_testing() -> Box<Session>;
+
+        fn load_values(
+            self: &'static Session,
+            label: &str,
+            relative_to: &str,
+            keys: &[&str],
+            scope: Pin<&mut Scope>,
+            settings: &Settings,
+            origin: ParseNodePtr,
+            err: Pin<&mut Err>,
+        );
+    }
 }
 
 pub use dummy::*;
diff --git a/src/gn/starlark/crates/ffi/src/errors.rs b/src/gn/starlark/crates/ffi/src/errors.rs
new file mode 100644
index 0000000..d76cb6d
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/errors.rs
@@ -0,0 +1,18 @@
+// 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 types::Label;
+
+/// Errors returned by the FFI layer.
+#[derive(thiserror::Error, Debug)]
+pub(crate) enum Error {
+    #[error("Key '{0}' not found in module '{1}'")]
+    KeyNotFound(String, Label),
+}
+
+impl From<Error> for starlark::Error {
+    fn from(err: Error) -> Self {
+        Self::new_other(err)
+    }
+}
diff --git a/src/gn/starlark/crates/ffi/src/eval_context.rs b/src/gn/starlark/crates/ffi/src/eval_context.rs
new file mode 100644
index 0000000..e514876
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/eval_context.rs
@@ -0,0 +1,82 @@
+// 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 allocative::Allocative;
+use starlark::values::ProvidesStaticType;
+use types::{LabelRef, PackageRef, PathResolver};
+
+#[derive(Allocative)]
+enum EvalContextKind {
+    BzlFile,
+}
+
+#[derive(Allocative, ProvidesStaticType)]
+pub struct EvalContext {
+    #[allocative(skip)]
+    session: &'static crate::session::Session,
+    #[allocative(skip)]
+    package: &'static PackageRef,
+    kind: EvalContextKind,
+}
+
+impl EvalContext {
+    pub fn new_bzl_file(
+        session: &'static crate::session::Session,
+        package: &'static PackageRef,
+    ) -> Self {
+        Self {
+            session,
+            package,
+            kind: EvalContextKind::BzlFile,
+        }
+    }
+}
+
+impl types::EvalContext for EvalContext {
+    type Scope = crate::scope::OwnedScope;
+    type Session = crate::session::Session;
+
+    fn current_package(&self) -> &types::PackageRef {
+        self.package
+    }
+
+    fn path_resolver(&self) -> &PathResolver {
+        &self.session.path_resolver
+    }
+
+    fn session(&self) -> &Self::Session {
+        self.session
+    }
+
+    fn current_toolchain(&self) -> LabelRef<'_> {
+        todo!()
+    }
+
+    fn require_macro(&self) -> starlark::Result<&Self::Scope> {
+        todo!()
+    }
+
+    fn require_bzl(&self) -> starlark::Result<()> {
+        todo!()
+    }
+
+    fn require_rule_impl(
+        &self,
+    ) -> starlark::Result<&mut types::CtxState<crate::target_ref::TargetRef>> {
+        todo!()
+    }
+}
+
+impl attr::traits::EvalContextAttrExt for EvalContext {
+    fn create_target(
+        &self,
+        _target_type: Option<types::OutputType>,
+        _target_name: &str,
+        _scope: &Self::Scope,
+        _rule: starlark::values::FrozenValue,
+        _attrs: Vec<attr::Attr>,
+    ) -> starlark::Result<<Self::Session as types::Session>::TargetRef> {
+        todo!()
+    }
+}
diff --git a/src/gn/starlark/crates/ffi/src/lib.rs b/src/gn/starlark/crates/ffi/src/lib.rs
index a27dbbd..2edea21 100644
--- a/src/gn/starlark/crates/ffi/src/lib.rs
+++ b/src/gn/starlark/crates/ffi/src/lib.rs
@@ -18,13 +18,17 @@
 //! these types in their own files.
 mod bridge;
 mod err;
+mod errors;
+mod eval_context;
 mod label;
 mod mutability;
 mod opaque;
 mod output_file;
 mod scope;
+mod session;
 mod settings;
 mod slice;
+mod target_ref;
 mod test_with_scope;
 mod value;
 
@@ -32,5 +36,6 @@
 pub use mutability::Immutable;
 pub use opaque::{NonOpaque, OpaqueSized};
 pub use scope::OwnedScope;
+pub use session::Session;
 pub use slice::{OwnedSlice, Slice};
 pub use test_with_scope::TestWithScope;
diff --git a/src/gn/starlark/crates/ffi/src/session.rs b/src/gn/starlark/crates/ffi/src/session.rs
new file mode 100644
index 0000000..a69d2b1
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/session.rs
@@ -0,0 +1,123 @@
+// 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 std::pin::Pin;
+
+use loader::FileLoader;
+use starlark::environment::{FrozenModule, Globals};
+use types::{
+    util::extend_lifetime, Label, LabelRef, PackageRef, PathResolver, Session as TypesSession,
+};
+
+use crate::errors::Error;
+
+/// Represents a Starlark evaluation session exposed to C++ via FFI.
+pub struct Session {
+    loader: FileLoader,
+    pub(crate) path_resolver: PathResolver,
+    globals: Globals,
+}
+
+fn make_attr_schema<'v>(
+    kind: attr::AttrKind,
+    args: attr::AttrSpecArgs<'v>,
+    eval: &mut starlark::eval::Evaluator<'v, '_, '_>,
+) -> starlark::Result<starlark::values::Value<'v>> {
+    use types::{EvalContext as _, EvaluatorContextExt as _};
+    let ctx = eval.context::<crate::eval_context::EvalContext>();
+    attr::AttrSchema::create(
+        kind,
+        args,
+        ctx.current_package(),
+        ctx.path_resolver(),
+        &eval.heap(),
+    )
+}
+
+fn build_globals() -> Globals {
+    let mut builder = starlark::environment::GlobalsBuilder::standard();
+    builder.set("attr", attr::AttrModule { make_attr_schema });
+    providers::globals::register_providers(&mut builder);
+    depset::depset_globals!(&mut builder, crate::eval_context::EvalContext);
+    rule::register_rule_globals!(&mut builder, crate::eval_context::EvalContext);
+    builder.build()
+}
+
+impl Session {
+    /// Creates a new `Session`.
+    pub fn from_resolver(path_resolver: PathResolver) -> Self {
+        Self {
+            loader: FileLoader::default(),
+            path_resolver,
+            globals: build_globals(),
+        }
+    }
+
+    /// Associated function for C++ constructor.
+    pub fn new(source_root: &str, source_root_rel: &str) -> Box<Self> {
+        Box::new(Self::from_resolver(PathResolver::new(
+            std::path::PathBuf::from(source_root),
+            source_root_rel.to_owned(),
+        )))
+    }
+
+    /// Associated function for C++ constructor.
+    pub fn new_for_testing() -> Box<Self> {
+        Box::new(Self::from_resolver(PathResolver::new_for_testing()))
+    }
+
+    fn load(&'static self, label: LabelRef<'_>) -> starlark::Result<FrozenModule> {
+        self.loader
+            .load(label, &self.path_resolver, &self.globals, &|pkg| {
+                // Safety: The package reference is guaranteed to live as long as the
+                // eval context.
+                crate::eval_context::EvalContext::new_bzl_file(self, unsafe {
+                    extend_lifetime(pkg)
+                })
+            })
+    }
+
+    /// Loads a Starlark module and populates a scope with values by key.
+    pub fn load_values(
+        &'static self,
+        label: &str,
+        relative_to: &str,
+        keys: &[&str],
+        mut scope: Pin<&mut crate::bridge::Scope>,
+        settings: &crate::Settings,
+        origin: crate::bridge::ParseNodePtr,
+        mut err: Pin<&mut crate::bridge::Err>,
+    ) {
+        err.as_mut().handle((|| -> starlark::Result<()> {
+            let label = Label::parse(label, PackageRef::new(relative_to)?)?;
+            let module = self.load(label.as_ref())?;
+
+            for key in keys {
+                let value = module
+                    .get(key)
+                    .map_err(|_| Error::KeyNotFound(key.to_string(), label.clone()))?;
+                let mut cxx_value = crate::bridge::SetValue(scope.as_mut(), key, origin);
+                cxx_value.as_mut().assign(value.value(), settings, origin);
+            }
+            Ok(())
+        })());
+    }
+}
+
+impl TypesSession for Session {
+    type TargetRef = crate::target_ref::TargetRef;
+
+    fn get_target(&self, _label: LabelRef<'_>, _toolchain: LabelRef<'_>) -> Self::TargetRef {
+        todo!()
+    }
+
+    fn register_dependency<'a>(
+        &self,
+        _source: Self::TargetRef,
+        _label: LabelRef<'a>,
+        _toolchain: LabelRef<'a>,
+    ) {
+        todo!()
+    }
+}
diff --git a/src/gn/starlark/crates/ffi/src/target_ref.rs b/src/gn/starlark/crates/ffi/src/target_ref.rs
new file mode 100644
index 0000000..08a4db0
--- /dev/null
+++ b/src/gn/starlark/crates/ffi/src/target_ref.rs
@@ -0,0 +1,65 @@
+// 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 allocative::Allocative;
+use starlark::values::{AllocValue, Heap, ProvidesStaticType, StarlarkValue, Value};
+use starlark_derive::{starlark_value, NoSerialize};
+use types::LabelRef;
+
+#[derive(Clone, Allocative, ProvidesStaticType, Debug, NoSerialize, PartialEq, Eq, Hash)]
+pub struct TargetRef;
+
+impl std::fmt::Display for TargetRef {
+    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        todo!()
+    }
+}
+
+impl types::IPromiseToImplementStarlarkEqAndHash for TargetRef {}
+
+#[starlark_value(type = "Target")]
+impl<'v> StarlarkValue<'v> for TargetRef {
+    fn equals(&self, _other: Value<'v>) -> starlark::Result<bool> {
+        todo!()
+    }
+
+    fn write_hash(
+        &self,
+        _hasher: &mut starlark::collections::StarlarkHasher,
+    ) -> starlark::Result<()> {
+        todo!()
+    }
+}
+
+impl<'v> AllocValue<'v> for TargetRef {
+    fn alloc_value(self, heap: Heap<'v>) -> Value<'v> {
+        heap.alloc_simple(self)
+    }
+}
+
+impl types::TargetRef for TargetRef {
+    fn label(&self) -> LabelRef<'_> {
+        todo!()
+    }
+
+    fn toolchain(&self) -> LabelRef<'_> {
+        todo!()
+    }
+
+    fn outputs(&self) -> Vec<types::File> {
+        todo!()
+    }
+
+    fn target_out_dir(&self, _prefix: &str, _suffix: &str, _separator: &str) -> String {
+        todo!()
+    }
+
+    fn register_dependencies<S: types::Session<TargetRef = Self>>(
+        &self,
+        _session: &S,
+        _toolchain: LabelRef<'_>,
+    ) {
+        todo!()
+    }
+}
diff --git a/src/gn/starlark/src/lib.rs b/src/gn/starlark/src/lib.rs
index b1216cc..99da268 100644
--- a/src/gn/starlark/src/lib.rs
+++ b/src/gn/starlark/src/lib.rs
@@ -1,3 +1,4 @@
 // 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.
+pub use ffi::*;