rust: Add stub support for rust_ffi modules

This adds stubs support for rust_ffi and rust_ffi_shared modules. Usage
should match current cc usage. The stubs generator leveraged is the cc
stubs generator.

Bug: 203478530
Test: m blueprint_tests
Change-Id: I043b9714a357cd5fe17c183ccdf86900f5172e0e
diff --git a/rust/library.go b/rust/library.go
index 9912c94..49169ac 100644
--- a/rust/library.go
+++ b/rust/library.go
@@ -25,6 +25,7 @@
 
 	"android/soong/android"
 	"android/soong/cc"
+	cc_config "android/soong/cc/config"
 )
 
 var (
@@ -79,9 +80,13 @@
 	// Whether this library is part of the Rust toolchain sysroot.
 	Sysroot *bool
 
-	// Exclude this rust_ffi target from being included in APEXes.
-	// TODO(b/362509506): remove this once stubs are properly supported by rust_ffi targets.
+	// Deprecated - exclude this rust_ffi target from being included in APEXes.
+	// TODO(b/362509506): remove this once all apex_exclude uses are switched to stubs.
 	Apex_exclude *bool
+
+	// Generate stubs to make this library accessible to APEXes.
+	// Can only be set for modules producing shared libraries.
+	Stubs cc.StubsProperties `android:"arch_variant"`
 }
 
 type LibraryMutatedProperties struct {
@@ -109,6 +114,15 @@
 
 	// Whether this library variant should be link libstd via rlibs
 	VariantIsStaticStd bool `blueprint:"mutated"`
+
+	// This variant is a stubs lib
+	BuildStubs bool `blueprint:"mutated"`
+	// This variant is the latest version
+	IsLatestVersion bool `blueprint:"mutated"`
+	// Version of the stubs lib
+	StubsVersion string `blueprint:"mutated"`
+	// List of all stubs versions associated with an implementation lib
+	AllStubsVersions []string `blueprint:"mutated"`
 }
 
 type libraryDecorator struct {
@@ -123,9 +137,28 @@
 
 	// table-of-contents file for cdylib crates to optimize out relinking when possible
 	tocFile android.OptionalPath
+
+	// Path to the file containing the APIs exported by this library
+	stubsSymbolFilePath    android.Path
+	apiListCoverageXmlPath android.ModuleOutPath
+	versionScriptPath      android.OptionalPath
+}
+
+func (library *libraryDecorator) stubs() bool {
+	return library.MutatedProperties.BuildStubs
+}
+
+func (library *libraryDecorator) setAPIListCoverageXMLPath(xml android.ModuleOutPath) {
+	library.apiListCoverageXmlPath = xml
+}
+
+func (library *libraryDecorator) libraryProperties() LibraryCompilerProperties {
+	return library.Properties
 }
 
 type libraryInterface interface {
+	cc.VersionedInterface
+
 	rlib() bool
 	dylib() bool
 	static() bool
@@ -161,6 +194,11 @@
 	BuildOnlyShared()
 
 	toc() android.OptionalPath
+
+	IsStubsImplementationRequired() bool
+	setAPIListCoverageXMLPath(out android.ModuleOutPath)
+
+	libraryProperties() LibraryCompilerProperties
 }
 
 func (library *libraryDecorator) nativeCoverage() bool {
@@ -276,58 +314,63 @@
 
 var _ compiler = (*libraryDecorator)(nil)
 var _ libraryInterface = (*libraryDecorator)(nil)
+var _ cc.VersionedInterface = (*libraryDecorator)(nil)
 var _ exportedFlagsProducer = (*libraryDecorator)(nil)
 var _ cc.VersionedInterface = (*libraryDecorator)(nil)
 
 func (library *libraryDecorator) HasLLNDKStubs() bool {
-	// Rust does not support LLNDK yet.
+	// Rust LLNDK is currently unsupported
 	return false
 }
 
 func (library *libraryDecorator) HasVendorPublicLibrary() bool {
-	// Rust does not support vendor public library.
+	// Rust does not support vendor_public_library yet.
 	return false
 }
 
 func (library *libraryDecorator) HasLLNDKHeaders() bool {
-	// Rust does not support LLNDK yet.
+	// Rust LLNDK is currently unsupported
 	return false
 }
 
 func (library *libraryDecorator) HasStubsVariants() bool {
-	return false
+	// Just having stubs.symbol_file is enough to create a stub variant. In that case
+	// the stub for the future API level is created.
+	return library.Properties.Stubs.Symbol_file != nil ||
+		len(library.Properties.Stubs.Versions) > 0
 }
 
 func (library *libraryDecorator) IsStubsImplementationRequired() bool {
-	return false
+	return BoolDefault(library.Properties.Stubs.Implementation_installable, true)
 }
 
 func (library *libraryDecorator) GetAPIListCoverageXMLPath() android.ModuleOutPath {
-	panic(fmt.Errorf("GetAPIListCoverageXMLPath called on unsupported Rust module"))
+	return library.apiListCoverageXmlPath
 }
 
 func (library *libraryDecorator) AllStubsVersions() []string {
-	panic(fmt.Errorf("AllStubsVersions called on unsupported Rust module"))
+	return library.MutatedProperties.AllStubsVersions
 }
 
 func (library *libraryDecorator) SetAllStubsVersions(versions []string) {
-	panic(fmt.Errorf("ApexSdkVersion called on unsupported Rust module"))
+	library.MutatedProperties.AllStubsVersions = versions
 }
 
 func (library *libraryDecorator) SetStubsVersion(version string) {
-	panic(fmt.Errorf("SetStubsVersion called on unsupported Rust module"))
+	library.MutatedProperties.StubsVersion = version
 }
 
 func (library *libraryDecorator) SetBuildStubs(isLatest bool) {
-	panic(fmt.Errorf("SetBuildStubs called on unsupported Rust module"))
+	library.MutatedProperties.BuildStubs = true
+	library.MutatedProperties.IsLatestVersion = isLatest
 }
 
 func (library *libraryDecorator) BuildStubs() bool {
-	return false
+	return library.MutatedProperties.BuildStubs
 }
 
 func (library *libraryDecorator) ImplementationModuleName(name string) string {
-	panic(fmt.Errorf("ImplementationModuleName called on unsupported Rust module"))
+	return name
 }
 
 func (library *libraryDecorator) IsLLNDKMovedToApex() bool {
@@ -335,12 +378,20 @@
 	return false
 }
 
-func (library *libraryDecorator) StubsVersions(ctx android.BaseModuleContext) []string {
-	panic(fmt.Errorf("StubsVersions called on unsupported Rust module"))
+func (library *libraryDecorator) StubsVersion() string {
+	return library.MutatedProperties.StubsVersion
 }
 
-func (library *libraryDecorator) StubsVersion() string {
-	panic(fmt.Errorf("StubsVersions called on unsupported Rust module"))
+// stubsVersions implements cc.VersionedInterface.
+func (library *libraryDecorator) StubsVersions(ctx android.BaseModuleContext) []string {
+	if !library.HasStubsVariants() {
+		return nil
+	}
+
+	// Future API level is implicitly added if there isn't
+	versions := cc.AddCurrentVersionIfNotPresent(library.Properties.Stubs.Versions)
+	cc.NormalizeVersions(ctx, versions)
+	return versions
 }
 
 // rust_library produces all Rust variants (rust_library_dylib and
@@ -420,6 +471,18 @@
 	return module.Init()
 }
 
+func CheckRustLibraryProperties(mctx android.DefaultableHookContext) {
+	lib := mctx.Module().(*Module).compiler.(libraryInterface)
+	if !lib.buildShared() {
+		if lib.libraryProperties().Stubs.Symbol_file != nil ||
+			lib.libraryProperties().Stubs.Implementation_installable != nil ||
+			len(lib.libraryProperties().Stubs.Versions) > 0 {
+
+			mctx.PropertyErrorf("stubs", "stubs properties can only be set for rust_ffi or rust_ffi_shared modules")
+		}
+	}
+}
+
 func (library *libraryDecorator) BuildOnlyFFI() {
 	library.MutatedProperties.BuildDylib = false
 	// we build rlibs for later static ffi linkage.
@@ -479,6 +542,7 @@
 
 	module.compiler = library
 
+	module.SetDefaultableHook(CheckRustLibraryProperties)
 	return module, library
 }
 
@@ -641,7 +705,11 @@
 	}
 
 	// Call the appropriate builder for this library type
-	if library.rlib() {
+	if library.stubs() {
+		ccFlags := library.getApiStubsCcFlags(ctx)
+		stubObjs := library.compileModuleLibApiStubs(ctx, ccFlags)
+		cc.BuildRustStubs(ctx, outputFile, deps.CrtBegin, deps.CrtEnd, stubObjs, ccFlags)
+	} else if library.rlib() {
 		ret.kytheFile = TransformSrctoRlib(ctx, crateRootPath, deps, flags, outputFile).kytheFile
 	} else if library.dylib() {
 		ret.kytheFile = TransformSrctoDylib(ctx, crateRootPath, deps, flags, outputFile).kytheFile
@@ -657,13 +725,13 @@
 	}
 
 	// Since we have FFI rlibs, we need to collect their includes as well
-	if library.static() || library.shared() || library.rlib() {
+	if library.static() || library.shared() || library.rlib() || library.stubs() {
 		android.SetProvider(ctx, cc.FlagExporterInfoProvider, cc.FlagExporterInfo{
 			IncludeDirs: android.FirstUniquePaths(library.includeDirs),
 		})
 	}
 
-	if library.shared() {
+	if library.shared() || library.stubs() {
 		// Optimize out relinking against shared libraries whose interface hasn't changed by
 		// depending on a table of contents file instead of the library itself.
 		tocFile := outputFile.ReplaceExtension(ctx, flags.Toolchain.SharedLibSuffix()[1:]+".toc")
@@ -674,9 +742,7 @@
 			TableOfContents: android.OptionalPathForPath(tocFile),
 			SharedLibrary:   outputFile,
 			Target:          ctx.Target(),
-			// TODO: when rust supports stubs uses the stubs state rather than inferring it from
-			//  apex_exclude.
-			IsStubs: Bool(library.Properties.Apex_exclude),
+			IsStubs:         library.BuildStubs(),
 		})
 	}
 
@@ -688,6 +754,7 @@
 			TransitiveStaticLibrariesForOrdering: depSet,
 		})
 	}
+	cc.AddStubDependencyProviders(ctx)
 
 	library.flagExporter.setProvider(ctx)
 
@@ -707,6 +774,53 @@
 	}
 }
 
+func (library *libraryDecorator) getApiStubsCcFlags(ctx ModuleContext) cc.Flags {
+	ccFlags := cc.Flags{}
+	toolchain := cc_config.FindToolchain(ctx.Os(), ctx.Arch())
+
+	platformSdkVersion := ""
+	if ctx.Device() {
+		platformSdkVersion = ctx.Config().PlatformSdkVersion().String()
+	}
+	minSdkVersion := cc.MinSdkVersion(ctx.RustModule(), cc.CtxIsForPlatform(ctx), ctx.Device(), platformSdkVersion)
+
+	// Collect common CC compilation flags
+	ccFlags = cc.CommonLinkerFlags(ctx, ccFlags, true, toolchain, false)
+	ccFlags = cc.CommonLibraryLinkerFlags(ctx, ccFlags, toolchain, library.getStem(ctx))
+	ccFlags = cc.AddStubLibraryCompilerFlags(ccFlags)
+	ccFlags = cc.AddTargetFlags(ctx, ccFlags, toolchain, minSdkVersion, false)
+
+	return ccFlags
+}
+
+func (library *libraryDecorator) compileModuleLibApiStubs(ctx ModuleContext, ccFlags cc.Flags) cc.Objects {
+	mod := ctx.RustModule()
+
+	symbolFile := String(library.Properties.Stubs.Symbol_file)
+	library.stubsSymbolFilePath = android.PathForModuleSrc(ctx, symbolFile)
+
+	apiParams := cc.ApiStubsParams{
+		NotInPlatform:  mod.NotInPlatform(),
+		IsNdk:          mod.IsNdk(ctx.Config()),
+		BaseModuleName: mod.BaseModuleName(),
+		ModuleName:     ctx.ModuleName(),
+	}
+	flag := cc.GetApiStubsFlags(apiParams)
+
+	nativeAbiResult := cc.ParseNativeAbiDefinition(ctx, symbolFile,
+		android.ApiLevelOrPanic(ctx, library.MutatedProperties.StubsVersion), flag)
+	objs := cc.CompileStubLibrary(ctx, ccFlags, nativeAbiResult.StubSrc, mod.getSharedFlags())
+
+	library.versionScriptPath = android.OptionalPathForPath(nativeAbiResult.VersionScript)
+
+	// Parse symbol file to get API list for coverage
+	if library.StubsVersion() == "current" && ctx.PrimaryArch() && !mod.InRecovery() && !mod.InProduct() && !mod.InVendor() {
+		library.apiListCoverageXmlPath = cc.ParseSymbolFileForAPICoverage(ctx, symbolFile)
+	}
+
+	return objs
+}
+
 func (library *libraryDecorator) rustdoc(ctx ModuleContext, flags Flags,
 	deps PathDeps) android.OptionalPath {
 	// rustdoc has builtin support for documenting config specific information
diff --git a/rust/library_test.go b/rust/library_test.go
index e5fd5e0..1198fcc 100644
--- a/rust/library_test.go
+++ b/rust/library_test.go
@@ -482,3 +482,289 @@
 			extra_exported_symbols: "libbar.map.txt",
 		}`)
 }
+
+func TestStubsVersions(t *testing.T) {
+	t.Parallel()
+	bp := `
+		rust_ffi {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.rs"],
+			stubs: {
+				versions: ["29", "R", "current"],
+			},
+		}
+	`
+	ctx := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture(),
+		android.FixtureModifyConfigAndContext(func(config android.Config, ctx *android.TestContext) {
+			config.TestProductVariables.Platform_version_active_codenames = []string{"R"}
+		})).RunTestWithBp(t, bp)
+
+	variants := ctx.ModuleVariantsForTests("libfoo")
+	for _, expectedVer := range []string{"29", "R", "current"} {
+		expectedVariant := "android_arm_armv7-a-neon_shared_" + expectedVer
+		if !android.InList(expectedVariant, variants) {
+			t.Errorf("missing expected variant: %q", expectedVariant)
+		}
+	}
+}
+
+func TestStubsVersions_NotSorted(t *testing.T) {
+	t.Parallel()
+	bp := `
+	rust_ffi_shared {
+		name: "libfoo",
+		crate_name: "foo",
+		srcs: ["foo.rs"],
+		stubs: {
+				versions: ["29", "current", "R"],
+			},
+		}
+	`
+	fixture := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture(),
+
+		android.FixtureModifyConfigAndContext(func(config android.Config, ctx *android.TestContext) {
+			config.TestProductVariables.Platform_version_active_codenames = []string{"R"}
+		}))
+
+	fixture.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(`"libfoo" .*: versions: not sorted`)).RunTestWithBp(t, bp)
+}
+
+func TestStubsVersions_ParseError(t *testing.T) {
+	t.Parallel()
+	bp := `
+	rust_ffi_shared {
+		name: "libfoo",
+		crate_name: "foo",
+		srcs: ["foo.rs"],
+			stubs: {
+				versions: ["29", "current", "X"],
+			},
+		}
+	`
+	fixture := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture(),
+
+		android.FixtureModifyConfigAndContext(func(config android.Config, ctx *android.TestContext) {
+			config.TestProductVariables.Platform_version_active_codenames = []string{"R"}
+		}))
+
+	fixture.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(`"libfoo" .*: versions: "X" could not be parsed as an integer and is not a recognized codename`)).RunTestWithBp(t, bp)
+}
+
+func TestVersionedStubs(t *testing.T) {
+	t.Parallel()
+	bp := `
+	rust_ffi_shared {
+		name: "libFoo",
+		crate_name: "Foo",
+		srcs: ["foo.rs"],
+			stubs: {
+				symbol_file: "foo.map.txt",
+				versions: ["1", "2", "3"],
+			},
+		}
+
+	cc_library_shared {
+		name: "libBar",
+		srcs: ["bar.c"],
+		shared_libs: ["libFoo#1"],
+	}
+
+	rust_library {
+		name: "libbar_rs",
+		crate_name: "bar_rs",
+		srcs: ["bar.rs"],
+		shared_libs: ["libFoo#1"],
+	}
+	rust_ffi {
+		name: "libbar_ffi_rs",
+		crate_name: "bar_ffi_rs",
+		srcs: ["bar.rs"],
+		shared_libs: ["libFoo#1"],
+	}
+	`
+
+	ctx := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture()).RunTestWithBp(t, bp)
+
+	variants := ctx.ModuleVariantsForTests("libFoo")
+	expectedVariants := []string{
+		"android_arm64_armv8-a_shared",
+		"android_arm64_armv8-a_shared_1",
+		"android_arm64_armv8-a_shared_2",
+		"android_arm64_armv8-a_shared_3",
+		"android_arm64_armv8-a_shared_current",
+		"android_arm_armv7-a-neon_shared",
+		"android_arm_armv7-a-neon_shared_1",
+		"android_arm_armv7-a-neon_shared_2",
+		"android_arm_armv7-a-neon_shared_3",
+		"android_arm_armv7-a-neon_shared_current",
+	}
+	variantsMismatch := false
+	if len(variants) != len(expectedVariants) {
+		variantsMismatch = true
+	} else {
+		for _, v := range expectedVariants {
+			if !android.InList(v, variants) {
+				variantsMismatch = false
+			}
+		}
+	}
+	if variantsMismatch {
+		t.Errorf("variants of libFoo expected:\n")
+		for _, v := range expectedVariants {
+			t.Errorf("%q\n", v)
+		}
+		t.Errorf(", but got:\n")
+		for _, v := range variants {
+			t.Errorf("%q\n", v)
+		}
+	}
+
+	libBarLinkRule := ctx.ModuleForTests("libBar", "android_arm64_armv8-a_shared").Rule("ld")
+	libBarFlags := libBarLinkRule.Args["libFlags"]
+
+	libBarRsRustcRule := ctx.ModuleForTests("libbar_rs", "android_arm64_armv8-a_dylib").Rule("rustc")
+	libBarRsFlags := libBarRsRustcRule.Args["linkFlags"]
+
+	libBarFfiRsRustcRule := ctx.ModuleForTests("libbar_ffi_rs", "android_arm64_armv8-a_shared").Rule("rustc")
+	libBarFfiRsFlags := libBarFfiRsRustcRule.Args["linkFlags"]
+
+	libFoo1StubPath := "libFoo/android_arm64_armv8-a_shared_1/unstripped/libFoo.so"
+	if !strings.Contains(libBarFlags, libFoo1StubPath) {
+		t.Errorf("%q is not found in %q", libFoo1StubPath, libBarFlags)
+	}
+	if !strings.Contains(libBarRsFlags, libFoo1StubPath) {
+		t.Errorf("%q is not found in %q", libFoo1StubPath, libBarRsFlags)
+	}
+	if !strings.Contains(libBarFfiRsFlags, libFoo1StubPath) {
+		t.Errorf("%q is not found in %q", libFoo1StubPath, libBarFfiRsFlags)
+	}
+}
+
+func TestCheckConflictingExplicitVersions(t *testing.T) {
+	t.Parallel()
+	bp := `
+	cc_library_shared {
+		name: "libbar",
+		srcs: ["bar.c"],
+		shared_libs: ["libfoo", "libfoo#impl"],
+	}
+
+	rust_ffi_shared {
+		name: "libfoo",
+		crate_name: "foo",
+		srcs: ["foo.rs"],
+		stubs: {
+			versions: ["29", "current"],
+		},
+	}
+	`
+	fixture := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture())
+
+	fixture.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(`duplicate shared libraries with different explicit versions`)).RunTestWithBp(t, bp)
+}
+
+func TestAddnoOverride64GlobalCflags(t *testing.T) {
+	t.Parallel()
+	bp := `
+		cc_library_shared {
+			name: "libclient",
+			srcs: ["foo.c"],
+			shared_libs: ["libfoo#1"],
+		}
+
+		rust_ffi_shared {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.c"],
+			shared_libs: ["libbar"],
+			stubs: {
+				symbol_file: "foo.map.txt",
+				versions: ["1", "2", "3"],
+			},
+		}
+
+		cc_library_shared {
+			name: "libbar",
+			export_include_dirs: ["include/libbar"],
+			srcs: ["foo.c"],
+		}`
+	ctx := android.GroupFixturePreparers(
+		prepareForRustTest,
+		android.PrepareForTestWithVisibility,
+		rustMockedFiles.AddToFixture()).RunTestWithBp(t, bp)
+
+	cFlags := ctx.ModuleForTests("libclient", "android_arm64_armv8-a_shared").Rule("cc").Args["cFlags"]
+
+	if !strings.Contains(cFlags, "${config.NoOverride64GlobalCflags}") {
+		t.Errorf("expected %q in cflags, got %q", "${config.NoOverride64GlobalCflags}", cFlags)
+	}
+}
+
+// Make sure the stubs properties can only be used in modules producing shared libs
+func TestRustStubsFFIOnly(t *testing.T) {
+	testRustError(t, "stubs properties", `
+		rust_library {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.c"],
+			shared_libs: ["libbar"],
+			stubs: {
+				symbol_file: "foo.map.txt",
+			},
+		}
+	`)
+
+	testRustError(t, "stubs properties", `
+		rust_library {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.c"],
+			shared_libs: ["libbar"],
+			stubs: {
+				versions: ["1"],
+			},
+		}
+	`)
+
+	testRustError(t, "stubs properties", `
+		rust_ffi_static {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.c"],
+			shared_libs: ["libbar"],
+			stubs: {
+				symbol_file: "foo.map.txt",
+			},
+		}
+	`)
+	testRustError(t, "stubs properties", `
+		rust_ffi_static {
+			name: "libfoo",
+			crate_name: "foo",
+			srcs: ["foo.c"],
+			shared_libs: ["libbar"],
+			stubs: {
+				versions: ["1"],
+			},
+		}
+	`)
+}
+
+// TODO: When rust_ffi libraries support export_*_lib_headers,
+// add a test similar to cc.TestStubsLibReexportsHeaders
diff --git a/rust/rust.go b/rust/rust.go
index 2332b52..557bdc3 100644
--- a/rust/rust.go
+++ b/rust/rust.go
@@ -162,9 +162,36 @@
 	// Make this module available when building for recovery
 	Recovery_available *bool
 
-	// Minimum sdk version that the artifact should support when it runs as part of mainline modules(APEX).
+	// The API level that this module is built against. The APIs of this API level will be
+	// visible at build time, but use of any APIs newer than min_sdk_version will render the
+	// module unloadable on older devices.  In the future it will be possible to weakly-link new
+	// APIs, making the behavior match Java: such modules will load on older devices, but
+	// calling new APIs on devices that do not support them will result in a crash.
+	//
+	// This property has the same behavior as sdk_version does for Java modules. For those
+	// familiar with Android Gradle, the property behaves similarly to how compileSdkVersion
+	// does for Java code.
+	//
+	// In addition, setting this property causes two variants to be built, one for the platform
+	// and one for apps.
+	Sdk_version *string
+
+	// Minimum OS API level supported by this C or C++ module. This property becomes the value
+	// of the __ANDROID_API__ macro. When the C or C++ module is included in an APEX or an APK,
+	// this property is also used to ensure that the min_sdk_version of the containing module is
+	// not older (i.e. less) than this module's min_sdk_version. When not set, this property
+	// defaults to the value of sdk_version.  When this is set to "apex_inherit", this tracks
+	// min_sdk_version of the containing APEX. When the module
+	// is not built for an APEX, "apex_inherit" defaults to sdk_version.
 	Min_sdk_version *string
 
+	// Variant is an SDK variant created by sdkMutator
+	IsSdkVariant bool `blueprint:"mutated"`
+
+	// Set by factories of module types that can only be referenced from variants compiled against
+	// the SDK.
+	AlwaysSdk bool `blueprint:"mutated"`
+
 	HideFromMake   bool `blueprint:"mutated"`
 	PreventInstall bool `blueprint:"mutated"`
 
@@ -209,6 +236,9 @@
 	apexSdkVersion android.ApiLevel
 
 	transitiveAndroidMkSharedLibs depset.DepSet[string]
+
+	// Shared flags among stubs build rules of this module
+	sharedFlags cc.SharedFlags
 }
 
 func (mod *Module) Header() bool {
@@ -374,7 +404,8 @@
 }
 
 func (mod *Module) IsVendorPublicLibrary() bool {
-	return mod.VendorProperties.IsVendorPublicLibrary
+	// Rust modules do not currently support vendor_public_library
+	return false
 }
 
 func (mod *Module) SdkAndPlatformVariantVisibleToMake() bool {
@@ -383,10 +414,12 @@
 }
 
 func (c *Module) IsVndkPrivate() bool {
+	// Rust modules do not currently support VNDK variants
 	return false
 }
 
 func (c *Module) IsLlndk() bool {
+	// Rust modules do not currently support LLNDK variants
 	return false
 }
 
@@ -395,31 +428,34 @@
 }
 
 func (m *Module) NeedsLlndkVariants() bool {
+	// Rust modules do not currently support LLNDK variants
 	return false
 }
 
 func (m *Module) NeedsVendorPublicLibraryVariants() bool {
+	// Rust modules do not currently support vendor_public_library
 	return false
 }
 
 func (mod *Module) HasLlndkStubs() bool {
+	// Rust modules do not currently support LLNDK stubs
 	return false
 }
 
 func (mod *Module) SdkVersion() string {
-	return ""
+	return String(mod.Properties.Sdk_version)
 }
 
 func (mod *Module) AlwaysSdk() bool {
-	return false
+	return mod.Properties.AlwaysSdk
 }
 
 func (mod *Module) IsSdkVariant() bool {
-	return false
+	return mod.Properties.IsSdkVariant
 }
 
 func (mod *Module) SplitPerApiLevel() bool {
-	return false
+	return cc.CanUseSdk(mod) && mod.IsCrt()
 }
 
 func (mod *Module) XrefRustFiles() android.Paths {
@@ -743,19 +779,42 @@
 }
 
 func (mod *Module) IsStubs() bool {
+	if lib, ok := mod.compiler.(libraryInterface); ok {
+		return lib.BuildStubs()
+	}
 	return false
 }
 
 func (mod *Module) HasStubsVariants() bool {
+	if lib, ok := mod.compiler.(libraryInterface); ok {
+		return lib.HasStubsVariants()
+	}
 	return false
 }
 
 func (mod *Module) ApexSdkVersion() android.ApiLevel {
-	panic(fmt.Errorf("ApexSdkVersion called on unsupported Rust module: %q", mod.BaseModuleName()))
+	return mod.apexSdkVersion
+}
+
+func (mod *Module) RustApexExclude() bool {
+	return mod.ApexExclude()
+}
+
+func (mod *Module) getSharedFlags() *cc.SharedFlags {
+	shared := &mod.sharedFlags
+	if shared.FlagsMap == nil {
+		shared.NumSharedFlags = 0
+		shared.FlagsMap = make(map[string]string)
+	}
+	return shared
 }
 
 func (mod *Module) ImplementationModuleNameForMake(ctx android.BaseModuleContext) string {
-	return mod.Name()
+	name := mod.BaseModuleName()
+	if versioned, ok := mod.compiler.(cc.VersionedInterface); ok {
+		name = versioned.ImplementationModuleName(name)
+	}
+	return name
 }
 
 func (mod *Module) Multilib() string {
@@ -903,7 +962,7 @@
 }
 
 func (mod *Module) SetSdkVersion(s string) {
-	panic(fmt.Errorf("SetSdkVersion called on unsupported Rust module: %q", mod.BaseModuleName()))
+	mod.Properties.Sdk_version = StringPtr(s)
 }
 
 func (mod *Module) SetMinSdkVersion(s string) {
@@ -1066,6 +1125,10 @@
 	linkableInfo.Shared = mod.Shared()
 	linkableInfo.CrateName = mod.CrateName()
 	linkableInfo.ExportedCrateLinkDirs = mod.ExportedCrateLinkDirs()
+	if lib, ok := mod.compiler.(cc.VersionedInterface); ok {
+		linkableInfo.StubsVersion = lib.StubsVersion()
+	}
+
 	android.SetProvider(ctx, cc.LinkableInfoProvider, linkableInfo)
 
 	rustInfo := &RustInfo{
@@ -1108,10 +1171,9 @@
 	// does compilation and linking in one step. If this changes in the future,
 	// move this as appropriate.
 	ccInfo.LinkerInfo = &cc.LinkerInfo{
-		WholeStaticLibs:      mod.compiler.baseCompilerProps().Whole_static_libs,
-		StaticLibs:           mod.compiler.baseCompilerProps().Static_libs,
-		SharedLibs:           mod.compiler.baseCompilerProps().Shared_libs,
-		UnstrippedOutputFile: mod.UnstrippedOutputFile(),
+		WholeStaticLibs: mod.compiler.baseCompilerProps().Whole_static_libs,
+		StaticLibs:      mod.compiler.baseCompilerProps().Static_libs,
+		SharedLibs:      mod.compiler.baseCompilerProps().Shared_libs,
 	}
 
 	android.SetProvider(ctx, cc.CcInfoProvider, ccInfo)
@@ -1277,6 +1339,21 @@
 	if mod.sanitize != nil {
 		mod.sanitize.begin(ctx)
 	}
+
+	if mod.UseSdk() && mod.IsSdkVariant() {
+		sdkVersion := ""
+		if ctx.Device() {
+			sdkVersion = mod.SdkVersion()
+		}
+		version, err := cc.NativeApiLevelFromUser(ctx, sdkVersion)
+		if err != nil {
+			ctx.PropertyErrorf("sdk_version", err.Error())
+			mod.Properties.Sdk_version = nil
+		} else {
+			mod.Properties.Sdk_version = StringPtr(version.String())
+		}
+	}
+
 }
 
 func (mod *Module) Prebuilt() *android.Prebuilt {
@@ -1552,9 +1629,12 @@
 				sharedLibraryInfo, exportedInfo := cc.ChooseStubOrImpl(ctx, dep)
 
 				if !sharedLibraryInfo.IsStubs {
-					depPaths.directImplementationDeps = append(depPaths.directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
-					if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
-						depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+					// TODO(b/362509506): remove this additional check once all apex_exclude uses are switched to stubs.
+					if !linkableInfo.RustApexExclude {
+						depPaths.directImplementationDeps = append(depPaths.directImplementationDeps, android.OutputFileForModule(ctx, dep, ""))
+						if info, ok := android.OtherModuleProvider(ctx, dep, cc.ImplementationDepInfoProvider); ok {
+							depPaths.transitiveImplementationDeps = append(depPaths.transitiveImplementationDeps, info.ImplementationDeps)
+						}
 					}
 				}
 
@@ -1922,7 +2002,7 @@
 var _ android.ApexModule = (*Module)(nil)
 
 // If a module is marked for exclusion from apexes, don't provide apex variants.
-// TODO(b/362509506): remove this once stubs are properly supported by rust_ffi targets.
+// TODO(b/362509506): remove this once all apex_exclude usages are removed.
 func (m *Module) CanHaveApexVariants() bool {
 	if m.ApexExclude() {
 		return false
@@ -1959,6 +2039,13 @@
 }
 
 // Implements android.ApexModule
+func (mod *Module) AlwaysRequiresPlatformApexVariant() bool {
+	// stub libraries and native bridge libraries are always available to platform
+	// TODO(b/362509506): remove the ApexExclude() check once all apex_exclude uses are switched to stubs.
+	return mod.IsStubs() || mod.Target().NativeBridge == android.NativeBridgeEnabled || mod.ApexExclude()
+}
+
+// Implements android.ApexModule
 func (mod *Module) OutgoingDepIsInSameApex(depTag blueprint.DependencyTag) bool {
 	if depTag == procMacroDepTag || depTag == customBindgenDepTag {
 		return false
@@ -1971,18 +2058,55 @@
 		return false
 	}
 
+	if depTag == cc.StubImplDepTag {
+		// We don't track from an implementation library to its stubs.
+		return false
+	}
+
+	if cc.ExcludeInApexDepTag(depTag) {
+		return false
+	}
+
+	// TODO(b/362509506): remove once all apex_exclude uses are switched to stubs.
+	if mod.ApexExclude() {
+		return false
+	}
+
 	return true
 }
 
 func (mod *Module) IncomingDepIsInSameApex(depTag blueprint.DependencyTag) bool {
-	return !mod.ApexExclude()
+	// TODO(b/362509506): remove once all apex_exclude uses are switched to stubs.
+	if mod.ApexExclude() {
+		return false
+	}
+
+	if mod.HasStubsVariants() {
+		if cc.IsSharedDepTag(depTag) {
+			// dynamic dep to a stubs lib crosses APEX boundary
+			return false
+		}
+		if cc.IsRuntimeDepTag(depTag) {
+			// runtime dep to a stubs lib also crosses APEX boundary
+			return false
+		}
+		if cc.IsHeaderDepTag(depTag) {
+			return false
+		}
+	}
+	return true
 }
 
 // Overrides ApexModule.IsInstallabeToApex()
 func (mod *Module) IsInstallableToApex() bool {
+	// TODO(b/362509506): remove once all apex_exclude uses are switched to stubs.
+	if mod.ApexExclude() {
+		return false
+	}
+
 	if mod.compiler != nil {
-		if lib, ok := mod.compiler.(libraryInterface); ok && (lib.shared() || lib.dylib()) {
-			return true
+		if lib, ok := mod.compiler.(libraryInterface); ok {
+			return (lib.shared() || lib.dylib()) && !lib.BuildStubs()
 		}
 		if _, ok := mod.compiler.(*binaryDecorator); ok {
 			return true
diff --git a/rust/testing.go b/rust/testing.go
index 0ce1b66..2082b52 100644
--- a/rust/testing.go
+++ b/rust/testing.go
@@ -80,7 +80,6 @@
 			no_libcrt: true,
 			nocrt: true,
 			system_shared_libs: [],
-			apex_available: ["//apex_available:platform", "//apex_available:anyapex"],
 			min_sdk_version: "29",
 			vendor_available: true,
 			host_supported: true,
@@ -88,6 +87,13 @@
 			llndk: {
 				symbol_file: "liblog.map.txt",
 			},
+			stubs: {
+				symbol_file: "liblog.map.txt",
+				versions: [
+					"29",
+					"30",
+				],
+			},
 		}
 		cc_library {
 			name: "libprotobuf-cpp-full",