Introduce a Bazel rule named `cc_bindings_from_rust`.

This is a small, incremental improvement over directly using `genrule`
in `cc_bindings_from_rs/test/functions/BUILD`:

1. It avoids having to redundantly mention "function.rs" twice: once as
   `srcs` of a `rust_library` rule, and once as `srcs` of `genrule`.

2. It lays the groundwork for working later on passing the actual
   rustc cmdline arguments to the `cc_bindings_from_rs` library
   (rather than hardcoding `--crate-type=lib`, `--codegen=panic=abort`,
   etc which for now happens before and after this CL).
   This is the main motivation for this CL - this future work is
   needed to unblock b/254097223.

3. It prevents _some_ duplication in the future - when introducing more
   end-to-end tests (i.e. golden tests) we can reuse the new rule
   (instead of duplicating the `genrule`).

PiperOrigin-RevId: 487261849
diff --git a/cc_bindings_from_rs/bazel_support/BUILD b/cc_bindings_from_rs/bazel_support/BUILD
new file mode 100644
index 0000000..3b88220
--- /dev/null
+++ b/cc_bindings_from_rs/bazel_support/BUILD
@@ -0,0 +1,14 @@
+"""Disclaimer: This project is experimental, under heavy development, and should not
+be used yet."""
+
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+licenses(["notice"])
+
+bzl_library(
+    name = "cc_bindings_from_rust_rule_bzl",
+    srcs = ["cc_bindings_from_rust_rule.bzl"],
+    visibility = [
+        "//:__subpackages__",
+    ],
+)
diff --git a/cc_bindings_from_rs/bazel_support/cc_bindings_from_rust_rule.bzl b/cc_bindings_from_rs/bazel_support/cc_bindings_from_rust_rule.bzl
new file mode 100644
index 0000000..1cc3eaf
--- /dev/null
+++ b/cc_bindings_from_rs/bazel_support/cc_bindings_from_rust_rule.bzl
@@ -0,0 +1,99 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+"""`cc_bindings_from_rust` rule.
+
+Disclaimer: This project is experimental, under heavy development, and should
+not be used yet.
+"""
+
+load(
+    "@rules_rust//rust:rust_common.bzl",
+    "CrateInfo",
+)
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
+
+def _generate_bindings(ctx, basename, crate_root, rustc_args):
+    # TODO(b/254097223): Also cover `rs_out_file = ... + "_cc_api_impl.rs"`.
+    h_out_file = ctx.actions.declare_file(basename + "_cc_api.h")
+
+    crubit_args = ctx.actions.args()
+    crubit_args.add("--h-out", h_out_file)
+
+    ctx.actions.run(
+        outputs = [h_out_file],
+        inputs = [crate_root],
+        executable = ctx.executable._cc_bindings_from_rs_tool,
+        mnemonic = "CcBindingsFromRust",
+        progress_message = "Generating C++ bindings from Rust: %s" % h_out_file,
+        arguments = [crubit_args, "--", rustc_args],
+    )
+
+    return h_out_file
+
+def _cc_bindings_from_rust_rule_impl(ctx):
+    basename = ctx.attr.crate.label.name
+    crate_root = ctx.attr.crate[CrateInfo].root
+
+    # TODO(b/258449205): Extract `rustc_args` from the target `crate` (instead
+    # of figuring out the `crate_root` and hard-coding `--crate-type`,
+    # `panic=abort`, etc.).  It seems that `BuildInfo` from
+    # @rules_rust//rust/private/providers.bzl is not
+    # exposed publicly?
+    rustc_args = ctx.actions.args()
+    rustc_args.add(crate_root)
+    rustc_args.add("--crate-type", "lib")
+    rustc_args.add("--codegen", "panic=abort")
+
+    h_out_file = _generate_bindings(ctx, basename, crate_root, rustc_args)
+
+    cc_toolchain = find_cpp_toolchain(ctx)
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+    )
+    (compilation_context, compilation_outputs) = cc_common.compile(
+        name = ctx.label.name,
+        actions = ctx.actions,
+        feature_configuration = feature_configuration,
+        cc_toolchain = cc_toolchain,
+        public_hdrs = [h_out_file],
+    )
+    (linking_context, _) = cc_common.create_linking_context_from_compilation_outputs(
+        name = ctx.label.name,
+        actions = ctx.actions,
+        feature_configuration = feature_configuration,
+        cc_toolchain = cc_toolchain,
+        compilation_outputs = compilation_outputs,
+        linking_contexts = [ctx.attr.crate[CcInfo].linking_context],
+    )
+    return [CcInfo(
+        compilation_context = compilation_context,
+        linking_context = linking_context,
+    )]
+
+# TODO(b/257283134): Register actions via an `aspect`, rather than directly
+# from the `rule` implementation?
+cc_bindings_from_rust = rule(
+    implementation = _cc_bindings_from_rust_rule_impl,
+    doc = "Rule for generating C++ bindings for a Rust library.",
+    attrs = {
+        "crate": attr.label(
+            doc = "Rust library to generate C++ bindings for",
+            allow_files = False,
+            mandatory = True,
+            providers = [CrateInfo],
+        ),
+        "_cc_bindings_from_rs_tool": attr.label(
+            default = Label("//cc_bindings_from_rs:cc_bindings_from_rs_legacy_toolchain_runner.sar"),
+            executable = True,
+            cfg = "exec",
+            allow_single_file = True,
+        ),
+        "_cc_toolchain": attr.label(
+            default = "@bazel_tools//tools/cpp:current_cc_toolchain",
+        ),
+    },
+    fragments = ["cpp"],
+)
diff --git a/cc_bindings_from_rs/test/functions/BUILD b/cc_bindings_from_rs/test/functions/BUILD
index 4ceb53e..ccface3 100644
--- a/cc_bindings_from_rs/test/functions/BUILD
+++ b/cc_bindings_from_rs/test/functions/BUILD
@@ -5,6 +5,10 @@
     "@rules_rust//rust:defs.bzl",
     "rust_library",
 )
+load(
+    "//cc_bindings_from_rs/bazel_support:cc_bindings_from_rust_rule.bzl",
+    "cc_bindings_from_rust",
+)
 
 licenses(["notice"])
 
@@ -17,37 +21,10 @@
     ],
 )
 
-alias(
-    name = "cc_bindings_from_rs_tool",
-    actual = "//cc_bindings_from_rs:cc_bindings_from_rs_legacy_toolchain_runner.sar",
-)
-
-# TODO(b/257283134): Replace `genrule` with a custom `rule`.
-genrule(
-    name = "functions_cc_api_genrule",
-    testonly = 1,
-    srcs = ["functions.rs"],
-
-    # TODO(b/254097223): Also cover `functions_cc_api_impl.rs`
-    # (wrapping it in a `rust_library` that `functions_cc_api` depends on).
-    outs = ["functions_cc_api.h"],
-    cmd = """
-        $(location :cc_bindings_from_rs_tool) \
-            "--h-out=$@" \
-            -- \
-            $(SRCS) \
-            --crate-type=lib \
-            --codegen=panic=abort \
-    """,
-    message = "Running cc_bindings_from_rs",
-    tools = [":cc_bindings_from_rs_tool"],
-)
-
-cc_library(
+cc_bindings_from_rust(
     name = "functions_cc_api",
     testonly = 1,
-    hdrs = [":functions_cc_api.h"],
-    deps = [":functions"],
+    crate = ":functions",
 )
 
 cc_test(