about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-05-05 08:36:17 +0000
committerbors <bors@rust-lang.org>2025-05-05 08:36:17 +0000
commit0eb0b8cb67ff2c37eab775eaac6b330347e5c97f (patch)
treeeff5adeca8d0888128642e55f3fecf2edd86790f
parent54d024e4bf462c77a86c4126d7e66d89b64f053a (diff)
parent14535312b522c0524dd94633cc6a49992b12cecd (diff)
downloadrust-0eb0b8cb67ff2c37eab775eaac6b330347e5c97f.tar.gz
rust-0eb0b8cb67ff2c37eab775eaac6b330347e5c97f.zip
Auto merge of #134767 - Bryanskiy:dylibs-3, r=petrochenkov
Initial support for dynamically linked crates

This PR is an initial implementation of [rust-lang/rfcs#3435](https://github.com/rust-lang/rfcs/pull/3435) proposal.
### component 1: interface generator

Interface generator - a tool for generating a stripped version of crate source code. The interface is like a C header, where all function bodies are omitted. For example, initial crate:

```rust
#[export]
#[repr(C)]
pub struct S {
   pub x: i32
}
#[export]
pub extern "C" fn foo(x: S) {
   m1::bar(x);
}

pub fn bar(x: crate::S) {
    // some computations
}
```

generated interface:

```rust
#[export]
#[repr(C)]
pub struct S {
    pub x: i32,
}

#[export]
pub extern "C" fn foo(x: S);

pub fn bar(x: crate::S);
```

The interface generator was implemented as part of the pretty-printer. Ideally interface should only contain exportable items, but here is the first problem:
-  pass for determining exportable items relies on privacy information, which is totally available only in HIR
- HIR pretty-printer uses pseudo-code(at least for attributes)

So, the interface generator was implemented in AST. This has led to the fact that non-exportable items cannot be filtered out, but I don't think this is a major issue at the moment.

To emit an interface use a new `sdylib` crate type which is basically the same as `dylib`, but it doesn't contain metadata, and also produces the interface as a second artifact. The current interface name is `lib{crate_name}.rs`.
#### Why was it decided to use a design with an auto-generated interface?

One of the main objectives of this proposal is to allow building the library and the application with different compiler versions. This requires either a metadata format compatible across rustc versions or some form of a source code. The option with a stable metadata format has not been investigated in detail, but it is not part of RFC either. Here is the the related discussion: https://github.com/rust-lang/rfcs/pull/3435#discussion_r1202872373

Original proposal suggests using the source code for the dynamic library and all its dependencies. Metadata is obtained from `cargo check`. I decided to use interface files since it is more or less compatible with the original proposal, but also allows users to hide the source code.
##### Regarding the design with interfaces

in Rust, files generally do not have a special meaning, unlike C++. A translation unit i.e. a crate is not a single file, it consists of modules. Modules, in turn, can be declared either in one file or divided into several. That's why the "interface file" isn't a very coherent concept in Rust. I would like to avoid adding an additional level of complexity for users until it is proven necessary. Therefore, the initial plan was to make the interfaces completely invisible to users i. e. make them auto-generated. I also planned to put them in the dylib, but this has not been done yet. (since the PR is already big enough, I decided to postpone it)

There is one concern, though, which has not yet been investigated(https://github.com/rust-lang/rust/pull/134767#issuecomment-2736471828):

> Compiling the interface as pretty-printed source code doesn't use correct macro hygiene (mostly relevant to macros 2.0, stable macros do not affect item hygiene).  I don't have much hope for encoding hygiene data in any stable way, we should rather support a way for the interface file to be provided manually, instead of being auto-generated, if there are any non-trivial requirements.
### component 2: crate loader

When building dynamic dependencies, the crate loader searches for the interface in the file system, builds the interface without codegen and loads it's metadata. Routing rules for interface files are almost the same as for `rlibs` and `dylibs`. Firstly, the compiler checks `extern` options and then tries to deduce the path himself.

Here are the code and commands that corresponds to the compilation process:

```rust
// simple-lib.rs
#![crate_type = "sdylib"]

#[extern]
pub extern "C" fn foo() -> i32 {
    42
}
```

```rust
// app.rs
extern crate simple_lib;

fn main() {
    assert!(simple_lib::foo(), 42);
}
```

```
// Generate interface, build library.
rustc +toolchain1 lib.rs

// Build app. Perhaps with a different compiler version.
rustc +toolchain2 app.rs -L.
```

P.S. The interface name/format and rules for file system routing can be changed further.
### component 3: exportable items collector

Query for collecting exportable items. Which items are exportable is defined [here](https://github.com/m-ou-se/rfcs/blob/export/text/0000-export.md#the-export-attribute) .
### component 4: "stable" mangling scheme

The mangling scheme proposed in the RFC consists of two parts: a mangled item path and a hash of the signature.
#### mangled item path

For the first part of the symbol it has been decided to reuse the `v0` mangling scheme as it much less dependent on compiler internals compared to the `legacy` scheme.

The exception is disambiguators (https://doc.rust-lang.org/rustc/symbol-mangling/v0.html#disambiguator):

For example, during symbol mangling rustc uses a special index to distinguish between two impls of the same type in the same module(See `DisambiguatedDefPathData`). The calculation of this index may depend on private items, but private items should not affect the ABI. Example:

```rust
#[export]
#[repr(C)]
pub struct S<T>(pub T);

struct S1;
pub struct S2;

impl S<S1> {
    extern "C" fn foo() -> i32 {
        1
    }
}

#[export]
impl S<S2> {
    // Different symbol names can be generated for this item
    // when compiling the interface and source code.
    pub extern "C" fn foo() -> i32 {
        2
    }
}
```

In order to make disambiguation independent of the compiler version we can assign an id to each impl according to their relative order in the source code.

The second example is `StableCrateId` which is used to disambiguate different crates. `StableCrateId` consists of crate name, `-Cmetadata` arguments and compiler version. At the moment, I have decided to keep only the crate name, but a more consistent approach to crate disambiguation could be added in the future.

Actually, there are more cases where such disambiguation can be used. For instance, when mangling internal rustc symbols, but it also hasn't been investigated in detail yet.
#### hash of the signature

Exportable functions from stable dylibs can be called from safe code. In order to provide type safety, 128 bit hash with relevant type information is appended to the symbol ([description from RFC](https://github.com/m-ou-se/rfcs/blob/export/text/0000-export.md#name-mangling-and-safety)). For now, it includes:

- hash of the type name for primitive types
- for ADT types with public fields the implementation follows [this](https://github.com/m-ou-se/rfcs/blob/export/text/0000-export.md#types-with-public-fields) rules

`#[export(unsafe_stable_abi = "hash")]` syntax for ADT types with private fields is not yet implemented.

Type safety is a subtle thing here. I used the approach from RFC, but there is the ongoing research project about it. [https://rust-lang.github.io/rust-project-goals/2025h1/safe-linking.html](https://rust-lang.github.io/rust-project-goals/2025h1/safe-linking.html)

### Unresolved questions

Interfaces:
1. Move the interface generator to HIR and add an exportable items filter.
2. Compatibility of auto-generated interfaces and macro hygiene.
3. There is an open issue with interface files compilation: https://github.com/rust-lang/rust/pull/134767#issuecomment-2736471828
4. Put an interface into a dylib.

Mangling scheme:
1. Which information is required to ensure type safety and how should it be encoded? ([https://rust-lang.github.io/rust-project-goals/2025h1/safe-linking.html](https://rust-lang.github.io/rust-project-goals/2025h1/safe-linking.html))
2. Determine all other possible cases, where path disambiguation is used. Make it compiler independent.

We also need a semi-stable API to represent types. For example, the order of fields in the `VariantDef` must be stable. Or a semi-stable representation for AST, which ensures that the order of the items in the code is preserved.

There are some others, mentioned in the proposal.
-rw-r--r--Cargo.lock1
-rw-r--r--compiler/rustc_ast_lowering/src/item.rs4
-rw-r--r--compiler/rustc_ast_passes/src/ast_validation.rs8
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/mod.rs4
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/state.rs35
-rw-r--r--compiler/rustc_ast_pretty/src/pprust/state/item.rs11
-rw-r--r--compiler/rustc_codegen_gcc/src/back/lto.rs6
-rw-r--r--compiler/rustc_codegen_llvm/src/back/lto.rs3
-rw-r--r--compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs6
-rw-r--r--compiler/rustc_codegen_ssa/src/back/link.rs14
-rw-r--r--compiler/rustc_codegen_ssa/src/back/linker.rs2
-rw-r--r--compiler/rustc_codegen_ssa/src/back/symbol_export.rs6
-rw-r--r--compiler/rustc_codegen_ssa/src/base.rs2
-rw-r--r--compiler/rustc_driver_impl/src/lib.rs7
-rw-r--r--compiler/rustc_feature/src/builtin_attrs.rs6
-rw-r--r--compiler/rustc_feature/src/unstable.rs2
-rw-r--r--compiler/rustc_interface/src/passes.rs28
-rw-r--r--compiler/rustc_metadata/Cargo.toml1
-rw-r--r--compiler/rustc_metadata/src/creader.rs5
-rw-r--r--compiler/rustc_metadata/src/dependency_format.rs71
-rw-r--r--compiler/rustc_metadata/src/locator.rs147
-rw-r--r--compiler/rustc_metadata/src/rmeta/decoder.rs11
-rw-r--r--compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs2
-rw-r--r--compiler/rustc_metadata/src/rmeta/encoder.rs21
-rw-r--r--compiler/rustc_metadata/src/rmeta/mod.rs2
-rw-r--r--compiler/rustc_middle/src/arena.rs2
-rw-r--r--compiler/rustc_middle/src/query/mod.rs10
-rw-r--r--compiler/rustc_middle/src/ty/context.rs14
-rw-r--r--compiler/rustc_middle/src/ty/mod.rs4
-rw-r--r--compiler/rustc_mir_build/src/builder/mod.rs4
-rw-r--r--compiler/rustc_passes/messages.ftl19
-rw-r--r--compiler/rustc_passes/src/check_attr.rs1
-rw-r--r--compiler/rustc_passes/src/check_export.rs398
-rw-r--r--compiler/rustc_passes/src/errors.rs54
-rw-r--r--compiler/rustc_passes/src/lang_items.rs7
-rw-r--r--compiler/rustc_passes/src/lib.rs2
-rw-r--r--compiler/rustc_passes/src/reachable.rs10
-rw-r--r--compiler/rustc_passes/src/weak_lang_items.rs3
-rw-r--r--compiler/rustc_session/src/config.rs12
-rw-r--r--compiler/rustc_session/src/cstore.rs1
-rw-r--r--compiler/rustc_session/src/options.rs2
-rw-r--r--compiler/rustc_session/src/output.rs8
-rw-r--r--compiler/rustc_span/src/symbol.rs2
-rw-r--r--compiler/rustc_symbol_mangling/src/export.rs181
-rw-r--r--compiler/rustc_symbol_mangling/src/lib.rs23
-rw-r--r--compiler/rustc_symbol_mangling/src/v0.rs20
-rw-r--r--rustfmt.toml1
-rw-r--r--tests/run-make/export/compile-interface-error/app.rs3
-rw-r--r--tests/run-make/export/compile-interface-error/liblibr.rs5
-rw-r--r--tests/run-make/export/compile-interface-error/rmake.rs9
-rw-r--r--tests/run-make/export/disambiguator/app.rs7
-rw-r--r--tests/run-make/export/disambiguator/libr.rs27
-rw-r--r--tests/run-make/export/disambiguator/rmake.rs12
-rw-r--r--tests/run-make/export/extern-opt/app.rs6
-rw-r--r--tests/run-make/export/extern-opt/libinterface.rs4
-rw-r--r--tests/run-make/export/extern-opt/libr.rs5
-rw-r--r--tests/run-make/export/extern-opt/rmake.rs23
-rw-r--r--tests/run-make/export/simple/app.rs8
-rw-r--r--tests/run-make/export/simple/libr.rs22
-rw-r--r--tests/run-make/export/simple/rmake.rs12
-rw-r--r--tests/ui/attributes/export/crate-type-2.rs2
-rw-r--r--tests/ui/attributes/export/crate-type-2.stderr9
-rw-r--r--tests/ui/attributes/export/crate-type.rs2
-rw-r--r--tests/ui/attributes/export/crate-type.stderr9
-rw-r--r--tests/ui/attributes/export/exportable.rs139
-rw-r--r--tests/ui/attributes/export/exportable.stderr130
-rw-r--r--tests/ui/attributes/export/lang-item.rs8
-rw-r--r--tests/ui/attributes/export/lang-item.stderr8
-rw-r--r--tests/ui/feature-gates/feature-gate-export_stable.rs5
-rw-r--r--tests/ui/feature-gates/feature-gate-export_stable.stderr13
70 files changed, 1534 insertions, 117 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 15fb32e0077..04cd44155a5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4036,6 +4036,7 @@ dependencies = [
  "rustc_session",
  "rustc_span",
  "rustc_target",
+ "tempfile",
  "tracing",
 ]
 
diff --git a/compiler/rustc_ast_lowering/src/item.rs b/compiler/rustc_ast_lowering/src/item.rs
index c009abd729d..f48a571b86a 100644
--- a/compiler/rustc_ast_lowering/src/item.rs
+++ b/compiler/rustc_ast_lowering/src/item.rs
@@ -1310,7 +1310,9 @@ impl<'hir> LoweringContext<'_, 'hir> {
             // create a fake body so that the entire rest of the compiler doesn't have to deal with
             // this as a special case.
             return self.lower_fn_body(decl, contract, |this| {
-                if attrs.iter().any(|a| a.has_name(sym::rustc_intrinsic)) {
+                if attrs.iter().any(|a| a.has_name(sym::rustc_intrinsic))
+                    || this.tcx.is_sdylib_interface_build()
+                {
                     let span = this.lower_span(span);
                     let empty_block = hir::Block {
                         hir_id: this.next_id(),
diff --git a/compiler/rustc_ast_passes/src/ast_validation.rs b/compiler/rustc_ast_passes/src/ast_validation.rs
index 9b64bcc6df4..cbf4f2f5eb2 100644
--- a/compiler/rustc_ast_passes/src/ast_validation.rs
+++ b/compiler/rustc_ast_passes/src/ast_validation.rs
@@ -84,6 +84,8 @@ struct AstValidator<'a> {
 
     lint_node_id: NodeId,
 
+    is_sdylib_interface: bool,
+
     lint_buffer: &'a mut LintBuffer,
 }
 
@@ -952,7 +954,7 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
                 self.check_defaultness(item.span, *defaultness);
 
                 let is_intrinsic = item.attrs.iter().any(|a| a.has_name(sym::rustc_intrinsic));
-                if body.is_none() && !is_intrinsic {
+                if body.is_none() && !is_intrinsic && !self.is_sdylib_interface {
                     self.dcx().emit_err(errors::FnWithoutBody {
                         span: item.span,
                         replace_span: self.ending_semi_or_hi(item.span),
@@ -1441,7 +1443,7 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
                     });
                 }
                 AssocItemKind::Fn(box Fn { body, .. }) => {
-                    if body.is_none() {
+                    if body.is_none() && !self.is_sdylib_interface {
                         self.dcx().emit_err(errors::AssocFnWithoutBody {
                             span: item.span,
                             replace_span: self.ending_semi_or_hi(item.span),
@@ -1689,6 +1691,7 @@ pub fn check_crate(
     sess: &Session,
     features: &Features,
     krate: &Crate,
+    is_sdylib_interface: bool,
     lints: &mut LintBuffer,
 ) -> bool {
     let mut validator = AstValidator {
@@ -1701,6 +1704,7 @@ pub fn check_crate(
         disallow_tilde_const: Some(TildeConstReason::Item),
         extern_mod_safety: None,
         lint_node_id: CRATE_NODE_ID,
+        is_sdylib_interface,
         lint_buffer: lints,
     };
     visit::walk_crate(&mut validator, krate);
diff --git a/compiler/rustc_ast_pretty/src/pprust/mod.rs b/compiler/rustc_ast_pretty/src/pprust/mod.rs
index 551506f2aef..a05e2bd6a5d 100644
--- a/compiler/rustc_ast_pretty/src/pprust/mod.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/mod.rs
@@ -7,7 +7,9 @@ use std::borrow::Cow;
 use rustc_ast as ast;
 use rustc_ast::token::{Token, TokenKind};
 use rustc_ast::tokenstream::{TokenStream, TokenTree};
-pub use state::{AnnNode, Comments, PpAnn, PrintState, State, print_crate};
+pub use state::{
+    AnnNode, Comments, PpAnn, PrintState, State, print_crate, print_crate_as_interface,
+};
 
 /// Print the token kind precisely, without converting `$crate` into its respective crate name.
 pub fn token_kind_to_string(tok: &TokenKind) -> Cow<'static, str> {
diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs
index 28d5eb87c27..0990c9b27eb 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state.rs
@@ -221,6 +221,7 @@ pub struct State<'a> {
     pub s: pp::Printer,
     comments: Option<Comments<'a>>,
     ann: &'a (dyn PpAnn + 'a),
+    is_sdylib_interface: bool,
 }
 
 const INDENT_UNIT: isize = 4;
@@ -237,9 +238,36 @@ pub fn print_crate<'a>(
     edition: Edition,
     g: &AttrIdGenerator,
 ) -> String {
+    let mut s = State {
+        s: pp::Printer::new(),
+        comments: Some(Comments::new(sm, filename, input)),
+        ann,
+        is_sdylib_interface: false,
+    };
+
+    print_crate_inner(&mut s, krate, is_expanded, edition, g);
+    s.s.eof()
+}
+
+pub fn print_crate_as_interface(
+    krate: &ast::Crate,
+    edition: Edition,
+    g: &AttrIdGenerator,
+) -> String {
     let mut s =
-        State { s: pp::Printer::new(), comments: Some(Comments::new(sm, filename, input)), ann };
+        State { s: pp::Printer::new(), comments: None, ann: &NoAnn, is_sdylib_interface: true };
 
+    print_crate_inner(&mut s, krate, false, edition, g);
+    s.s.eof()
+}
+
+fn print_crate_inner<'a>(
+    s: &mut State<'a>,
+    krate: &ast::Crate,
+    is_expanded: bool,
+    edition: Edition,
+    g: &AttrIdGenerator,
+) {
     // We need to print shebang before anything else
     // otherwise the resulting code will not compile
     // and shebang will be useless.
@@ -282,8 +310,7 @@ pub fn print_crate<'a>(
         s.print_item(item);
     }
     s.print_remaining_comments();
-    s.ann.post(&mut s, AnnNode::Crate(krate));
-    s.s.eof()
+    s.ann.post(s, AnnNode::Crate(krate));
 }
 
 /// Should two consecutive tokens be printed with a space between them?
@@ -1111,7 +1138,7 @@ impl<'a> PrintState<'a> for State<'a> {
 
 impl<'a> State<'a> {
     pub fn new() -> State<'a> {
-        State { s: pp::Printer::new(), comments: None, ann: &NoAnn }
+        State { s: pp::Printer::new(), comments: None, ann: &NoAnn, is_sdylib_interface: false }
     }
 
     fn commasep_cmnt<T, F, G>(&mut self, b: Breaks, elts: &[T], mut op: F, mut get_span: G)
diff --git a/compiler/rustc_ast_pretty/src/pprust/state/item.rs b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
index 1e02ac8fd5d..70cf2f2a459 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state/item.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
@@ -160,6 +160,10 @@ impl<'a> State<'a> {
 
     /// Pretty-prints an item.
     pub(crate) fn print_item(&mut self, item: &ast::Item) {
+        if self.is_sdylib_interface && item.span.is_dummy() {
+            // Do not print prelude for interface files.
+            return;
+        }
         self.hardbreak_if_not_bol();
         self.maybe_print_comment(item.span.lo());
         self.print_outer_attributes(&item.attrs);
@@ -682,6 +686,13 @@ impl<'a> State<'a> {
             self.print_contract(contract);
         }
         if let Some((body, (cb, ib))) = body_cb_ib {
+            if self.is_sdylib_interface {
+                self.word(";");
+                self.end(ib); // end inner head-block
+                self.end(cb); // end outer head-block
+                return;
+            }
+
             self.nbsp();
             self.print_block_with_attrs(body, attrs, cb, ib);
         } else {
diff --git a/compiler/rustc_codegen_gcc/src/back/lto.rs b/compiler/rustc_codegen_gcc/src/back/lto.rs
index e5221c7da31..faeb2643ecb 100644
--- a/compiler/rustc_codegen_gcc/src/back/lto.rs
+++ b/compiler/rustc_codegen_gcc/src/back/lto.rs
@@ -44,7 +44,11 @@ use crate::{GccCodegenBackend, GccContext, SyncContext, to_gcc_opt_level};
 
 pub fn crate_type_allows_lto(crate_type: CrateType) -> bool {
     match crate_type {
-        CrateType::Executable | CrateType::Dylib | CrateType::Staticlib | CrateType::Cdylib => true,
+        CrateType::Executable
+        | CrateType::Dylib
+        | CrateType::Staticlib
+        | CrateType::Cdylib
+        | CrateType::Sdylib => true,
         CrateType::Rlib | CrateType::ProcMacro => false,
     }
 }
diff --git a/compiler/rustc_codegen_llvm/src/back/lto.rs b/compiler/rustc_codegen_llvm/src/back/lto.rs
index 39b3a23e0b1..cb329323f5d 100644
--- a/compiler/rustc_codegen_llvm/src/back/lto.rs
+++ b/compiler/rustc_codegen_llvm/src/back/lto.rs
@@ -42,7 +42,8 @@ fn crate_type_allows_lto(crate_type: CrateType) -> bool {
         | CrateType::Dylib
         | CrateType::Staticlib
         | CrateType::Cdylib
-        | CrateType::ProcMacro => true,
+        | CrateType::ProcMacro
+        | CrateType::Sdylib => true,
         CrateType::Rlib => false,
     }
 }
diff --git a/compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs b/compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs
index 4ffe551df09..8f0948b8183 100644
--- a/compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs
+++ b/compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs
@@ -95,7 +95,11 @@ pub(crate) fn needs_gdb_debug_scripts_section(cx: &CodegenCx<'_, '_>) -> bool {
     // in the `.debug_gdb_scripts` section. For that reason, we make sure that the
     // section is only emitted for leaf crates.
     let embed_visualizers = cx.tcx.crate_types().iter().any(|&crate_type| match crate_type {
-        CrateType::Executable | CrateType::Dylib | CrateType::Cdylib | CrateType::Staticlib => {
+        CrateType::Executable
+        | CrateType::Dylib
+        | CrateType::Cdylib
+        | CrateType::Staticlib
+        | CrateType::Sdylib => {
             // These are crate types for which we will embed pretty printers since they
             // are treated as leaf crates.
             true
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index 323538969d7..159c17b0af7 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -1053,9 +1053,10 @@ fn link_natively(
                 strip_with_external_utility(sess, stripcmd, out_filename, &["--strip-debug"])
             }
             // Per the manpage, `-x` is the maximum safe strip level for dynamic libraries. (#93988)
-            (Strip::Symbols, CrateType::Dylib | CrateType::Cdylib | CrateType::ProcMacro) => {
-                strip_with_external_utility(sess, stripcmd, out_filename, &["-x"])
-            }
+            (
+                Strip::Symbols,
+                CrateType::Dylib | CrateType::Cdylib | CrateType::ProcMacro | CrateType::Sdylib,
+            ) => strip_with_external_utility(sess, stripcmd, out_filename, &["-x"]),
             (Strip::Symbols, _) => {
                 strip_with_external_utility(sess, stripcmd, out_filename, &["--strip-all"])
             }
@@ -1243,8 +1244,10 @@ fn add_sanitizer_libraries(
     // which should be linked to both executables and dynamic libraries.
     // Everywhere else the runtimes are currently distributed as static
     // libraries which should be linked to executables only.
-    if matches!(crate_type, CrateType::Dylib | CrateType::Cdylib | CrateType::ProcMacro)
-        && !(sess.target.is_like_darwin || sess.target.is_like_msvc)
+    if matches!(
+        crate_type,
+        CrateType::Dylib | CrateType::Cdylib | CrateType::ProcMacro | CrateType::Sdylib
+    ) && !(sess.target.is_like_darwin || sess.target.is_like_msvc)
     {
         return;
     }
@@ -1938,6 +1941,7 @@ fn add_late_link_args(
     codegen_results: &CodegenResults,
 ) {
     let any_dynamic_crate = crate_type == CrateType::Dylib
+        || crate_type == CrateType::Sdylib
         || codegen_results.crate_info.dependency_formats.iter().any(|(ty, list)| {
             *ty == crate_type && list.iter().any(|&linkage| linkage == Linkage::Dynamic)
         });
diff --git a/compiler/rustc_codegen_ssa/src/back/linker.rs b/compiler/rustc_codegen_ssa/src/back/linker.rs
index e1f903726fb..8fc83908efb 100644
--- a/compiler/rustc_codegen_ssa/src/back/linker.rs
+++ b/compiler/rustc_codegen_ssa/src/back/linker.rs
@@ -1817,7 +1817,7 @@ pub(crate) fn linked_symbols(
     crate_type: CrateType,
 ) -> Vec<(String, SymbolExportKind)> {
     match crate_type {
-        CrateType::Executable | CrateType::Cdylib | CrateType::Dylib => (),
+        CrateType::Executable | CrateType::Cdylib | CrateType::Dylib | CrateType::Sdylib => (),
         CrateType::Staticlib | CrateType::ProcMacro | CrateType::Rlib => {
             return Vec::new();
         }
diff --git a/compiler/rustc_codegen_ssa/src/back/symbol_export.rs b/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
index 50fb08b2868..1bfdbc0b620 100644
--- a/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
+++ b/compiler/rustc_codegen_ssa/src/back/symbol_export.rs
@@ -29,7 +29,7 @@ fn crate_export_threshold(crate_type: CrateType) -> SymbolExportLevel {
         CrateType::Executable | CrateType::Staticlib | CrateType::ProcMacro | CrateType::Cdylib => {
             SymbolExportLevel::C
         }
-        CrateType::Rlib | CrateType::Dylib => SymbolExportLevel::Rust,
+        CrateType::Rlib | CrateType::Dylib | CrateType::Sdylib => SymbolExportLevel::Rust,
     }
 }
 
@@ -45,7 +45,7 @@ pub fn crates_export_threshold(crate_types: &[CrateType]) -> SymbolExportLevel {
 }
 
 fn reachable_non_generics_provider(tcx: TyCtxt<'_>, _: LocalCrate) -> DefIdMap<SymbolExportInfo> {
-    if !tcx.sess.opts.output_types.should_codegen() {
+    if !tcx.sess.opts.output_types.should_codegen() && !tcx.is_sdylib_interface_build() {
         return Default::default();
     }
 
@@ -168,7 +168,7 @@ fn exported_symbols_provider_local<'tcx>(
     tcx: TyCtxt<'tcx>,
     _: LocalCrate,
 ) -> &'tcx [(ExportedSymbol<'tcx>, SymbolExportInfo)] {
-    if !tcx.sess.opts.output_types.should_codegen() {
+    if !tcx.sess.opts.output_types.should_codegen() && !tcx.is_sdylib_interface_build() {
         return &[];
     }
 
diff --git a/compiler/rustc_codegen_ssa/src/base.rs b/compiler/rustc_codegen_ssa/src/base.rs
index f5480da2808..89439e40937 100644
--- a/compiler/rustc_codegen_ssa/src/base.rs
+++ b/compiler/rustc_codegen_ssa/src/base.rs
@@ -1092,7 +1092,7 @@ impl CrateInfo {
         }
 
         let embed_visualizers = tcx.crate_types().iter().any(|&crate_type| match crate_type {
-            CrateType::Executable | CrateType::Dylib | CrateType::Cdylib => {
+            CrateType::Executable | CrateType::Dylib | CrateType::Cdylib | CrateType::Sdylib => {
                 // These are crate types for which we invoke the linker and can embed
                 // NatVis visualizers.
                 true
diff --git a/compiler/rustc_driver_impl/src/lib.rs b/compiler/rustc_driver_impl/src/lib.rs
index d18fa892814..fdf8053b15a 100644
--- a/compiler/rustc_driver_impl/src/lib.rs
+++ b/compiler/rustc_driver_impl/src/lib.rs
@@ -54,8 +54,8 @@ use rustc_metadata::locator;
 use rustc_middle::ty::TyCtxt;
 use rustc_parse::{new_parser_from_file, new_parser_from_source_str, unwrap_or_emit_fatal};
 use rustc_session::config::{
-    CG_OPTIONS, ErrorOutputType, Input, OptionDesc, OutFileName, OutputType, UnstableOptions,
-    Z_OPTIONS, nightly_options, parse_target_triple,
+    CG_OPTIONS, CrateType, ErrorOutputType, Input, OptionDesc, OutFileName, OutputType,
+    UnstableOptions, Z_OPTIONS, nightly_options, parse_target_triple,
 };
 use rustc_session::getopts::{self, Matches};
 use rustc_session::lint::{Lint, LintId};
@@ -352,6 +352,8 @@ pub fn run_compiler(at_args: &[String], callbacks: &mut (dyn Callbacks + Send))
 
             passes::write_dep_info(tcx);
 
+            passes::write_interface(tcx);
+
             if sess.opts.output_types.contains_key(&OutputType::DepInfo)
                 && sess.opts.output_types.len() == 1
             {
@@ -816,6 +818,7 @@ fn print_crate_info(
                 let supported_crate_types = CRATE_TYPES
                     .iter()
                     .filter(|(_, crate_type)| !invalid_output_for_target(&sess, *crate_type))
+                    .filter(|(_, crate_type)| *crate_type != CrateType::Sdylib)
                     .map(|(crate_type_sym, _)| *crate_type_sym)
                     .collect::<BTreeSet<_>>();
                 for supported_crate_type in supported_crate_types {
diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs
index a5e6b1c00d6..c117e0fcf7c 100644
--- a/compiler/rustc_feature/src/builtin_attrs.rs
+++ b/compiler/rustc_feature/src/builtin_attrs.rs
@@ -536,6 +536,12 @@ pub static BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
     // Unstable attributes:
     // ==========================================================================
 
+    // Linking:
+    gated!(
+        export_stable, Normal, template!(Word), WarnFollowing,
+        EncodeCrossCrate::No, experimental!(export_stable)
+    ),
+
     // Testing:
     gated!(
         test_runner, CrateLevel, template!(List: "path"), ErrorFollowing,
diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs
index 1a011dfff3f..f1bc2c5ea88 100644
--- a/compiler/rustc_feature/src/unstable.rs
+++ b/compiler/rustc_feature/src/unstable.rs
@@ -485,6 +485,8 @@ declare_features! (
     (unstable, explicit_extern_abis, "CURRENT_RUSTC_VERSION", Some(134986)),
     /// Allows explicit tail calls via `become` expression.
     (incomplete, explicit_tail_calls, "1.72.0", Some(112788)),
+    /// Allows using `#[export_stable]` which indicates that an item is exportable.
+    (incomplete, export_stable, "CURRENT_RUSTC_VERSION", Some(139939)),
     /// Allows using `aapcs`, `efiapi`, `sysv64` and `win64` as calling conventions
     /// for functions with varargs.
     (unstable, extended_varargs_abi_support, "1.65.0", Some(100189)),
diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs
index 493b1d5eaa9..f4d11a7c0be 100644
--- a/compiler/rustc_interface/src/passes.rs
+++ b/compiler/rustc_interface/src/passes.rs
@@ -31,10 +31,11 @@ use rustc_resolve::Resolver;
 use rustc_session::config::{CrateType, Input, OutFileName, OutputFilenames, OutputType};
 use rustc_session::cstore::Untracked;
 use rustc_session::output::{collect_crate_types, filename_for_input};
+use rustc_session::parse::feature_err;
 use rustc_session::search_paths::PathKind;
 use rustc_session::{Limit, Session};
 use rustc_span::{
-    ErrorGuaranteed, FileName, SourceFileHash, SourceFileHashAlgorithm, Span, Symbol, sym,
+    DUMMY_SP, ErrorGuaranteed, FileName, SourceFileHash, SourceFileHashAlgorithm, Span, Symbol, sym,
 };
 use rustc_target::spec::PanicStrategy;
 use rustc_trait_selection::traits;
@@ -237,6 +238,7 @@ fn configure_and_expand(
             sess,
             features,
             &krate,
+            tcx.is_sdylib_interface_build(),
             resolver.lint_buffer(),
         )
     });
@@ -253,6 +255,9 @@ fn configure_and_expand(
             sess.dcx().emit_err(errors::MixedProcMacroCrate);
         }
     }
+    if crate_types.contains(&CrateType::Sdylib) && !tcx.features().export_stable() {
+        feature_err(sess, sym::export_stable, DUMMY_SP, "`sdylib` crate type is unstable").emit();
+    }
 
     if is_proc_macro_crate && sess.panic_strategy() == PanicStrategy::Abort {
         sess.dcx().emit_warn(errors::ProcMacroCratePanicAbort);
@@ -742,6 +747,25 @@ pub fn write_dep_info(tcx: TyCtxt<'_>) {
     }
 }
 
+pub fn write_interface<'tcx>(tcx: TyCtxt<'tcx>) {
+    if !tcx.crate_types().contains(&rustc_session::config::CrateType::Sdylib) {
+        return;
+    }
+    let _timer = tcx.sess.timer("write_interface");
+    let (_, krate) = &*tcx.resolver_for_lowering().borrow();
+
+    let krate = rustc_ast_pretty::pprust::print_crate_as_interface(
+        krate,
+        tcx.sess.psess.edition,
+        &tcx.sess.psess.attr_id_generator,
+    );
+    let export_output = tcx.output_filenames(()).interface_path();
+    let mut file = fs::File::create_buffered(export_output).unwrap();
+    if let Err(err) = write!(file, "{}", krate) {
+        tcx.dcx().fatal(format!("error writing interface file: {}", err));
+    }
+}
+
 pub static DEFAULT_QUERY_PROVIDERS: LazyLock<Providers> = LazyLock::new(|| {
     let providers = &mut Providers::default();
     providers.analysis = analysis;
@@ -930,6 +954,8 @@ fn run_required_analyses(tcx: TyCtxt<'_>) {
                 CStore::from_tcx(tcx).report_unused_deps(tcx);
             },
             {
+                tcx.ensure_ok().exportable_items(LOCAL_CRATE);
+                tcx.ensure_ok().stable_order_of_exportable_impls(LOCAL_CRATE);
                 tcx.par_hir_for_each_module(|module| {
                     tcx.ensure_ok().check_mod_loops(module);
                     tcx.ensure_ok().check_mod_attrs(module);
diff --git a/compiler/rustc_metadata/Cargo.toml b/compiler/rustc_metadata/Cargo.toml
index b11f9260be7..26878c488b7 100644
--- a/compiler/rustc_metadata/Cargo.toml
+++ b/compiler/rustc_metadata/Cargo.toml
@@ -26,6 +26,7 @@ rustc_serialize = { path = "../rustc_serialize" }
 rustc_session = { path = "../rustc_session" }
 rustc_span = { path = "../rustc_span" }
 rustc_target = { path = "../rustc_target" }
+tempfile = "3.7.1"
 tracing = "0.1"
 # tidy-alphabetical-end
 
diff --git a/compiler/rustc_metadata/src/creader.rs b/compiler/rustc_metadata/src/creader.rs
index 1c3222bbfeb..07fb2de8a3e 100644
--- a/compiler/rustc_metadata/src/creader.rs
+++ b/compiler/rustc_metadata/src/creader.rs
@@ -148,7 +148,7 @@ impl<'a> std::fmt::Debug for CrateDump<'a> {
             writeln!(fmt, "  hash: {}", data.hash())?;
             writeln!(fmt, "  reqd: {:?}", data.dep_kind())?;
             writeln!(fmt, "  priv: {:?}", data.is_private_dep())?;
-            let CrateSource { dylib, rlib, rmeta } = data.source();
+            let CrateSource { dylib, rlib, rmeta, sdylib_interface } = data.source();
             if let Some(dylib) = dylib {
                 writeln!(fmt, "  dylib: {}", dylib.0.display())?;
             }
@@ -158,6 +158,9 @@ impl<'a> std::fmt::Debug for CrateDump<'a> {
             if let Some(rmeta) = rmeta {
                 writeln!(fmt, "   rmeta: {}", rmeta.0.display())?;
             }
+            if let Some(sdylib_interface) = sdylib_interface {
+                writeln!(fmt, "   sdylib interface: {}", sdylib_interface.0.display())?;
+            }
         }
         Ok(())
     }
diff --git a/compiler/rustc_metadata/src/dependency_format.rs b/compiler/rustc_metadata/src/dependency_format.rs
index be31aa629c8..fcae33c73c9 100644
--- a/compiler/rustc_metadata/src/dependency_format.rs
+++ b/compiler/rustc_metadata/src/dependency_format.rs
@@ -88,45 +88,42 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
         return IndexVec::new();
     }
 
-    let preferred_linkage = match ty {
-        // Generating a dylib without `-C prefer-dynamic` means that we're going
-        // to try to eagerly statically link all dependencies. This is normally
-        // done for end-product dylibs, not intermediate products.
-        //
-        // Treat cdylibs and staticlibs similarly. If `-C prefer-dynamic` is set,
-        // the caller may be code-size conscious, but without it, it makes sense
-        // to statically link a cdylib or staticlib. For staticlibs we use
-        // `-Z staticlib-prefer-dynamic` for now. This may be merged into
-        // `-C prefer-dynamic` in the future.
-        CrateType::Dylib | CrateType::Cdylib => {
-            if sess.opts.cg.prefer_dynamic {
-                Linkage::Dynamic
-            } else {
-                Linkage::Static
+    let preferred_linkage =
+        match ty {
+            // Generating a dylib without `-C prefer-dynamic` means that we're going
+            // to try to eagerly statically link all dependencies. This is normally
+            // done for end-product dylibs, not intermediate products.
+            //
+            // Treat cdylibs and staticlibs similarly. If `-C prefer-dynamic` is set,
+            // the caller may be code-size conscious, but without it, it makes sense
+            // to statically link a cdylib or staticlib. For staticlibs we use
+            // `-Z staticlib-prefer-dynamic` for now. This may be merged into
+            // `-C prefer-dynamic` in the future.
+            CrateType::Dylib | CrateType::Cdylib | CrateType::Sdylib => {
+                if sess.opts.cg.prefer_dynamic { Linkage::Dynamic } else { Linkage::Static }
             }
-        }
-        CrateType::Staticlib => {
-            if sess.opts.unstable_opts.staticlib_prefer_dynamic {
-                Linkage::Dynamic
-            } else {
-                Linkage::Static
+            CrateType::Staticlib => {
+                if sess.opts.unstable_opts.staticlib_prefer_dynamic {
+                    Linkage::Dynamic
+                } else {
+                    Linkage::Static
+                }
             }
-        }
 
-        // If the global prefer_dynamic switch is turned off, or the final
-        // executable will be statically linked, prefer static crate linkage.
-        CrateType::Executable if !sess.opts.cg.prefer_dynamic || sess.crt_static(Some(ty)) => {
-            Linkage::Static
-        }
-        CrateType::Executable => Linkage::Dynamic,
+            // If the global prefer_dynamic switch is turned off, or the final
+            // executable will be statically linked, prefer static crate linkage.
+            CrateType::Executable if !sess.opts.cg.prefer_dynamic || sess.crt_static(Some(ty)) => {
+                Linkage::Static
+            }
+            CrateType::Executable => Linkage::Dynamic,
 
-        // proc-macro crates are mostly cdylibs, but we also need metadata.
-        CrateType::ProcMacro => Linkage::Static,
+            // proc-macro crates are mostly cdylibs, but we also need metadata.
+            CrateType::ProcMacro => Linkage::Static,
 
-        // No linkage happens with rlibs, we just needed the metadata (which we
-        // got long ago), so don't bother with anything.
-        CrateType::Rlib => Linkage::NotLinked,
-    };
+            // No linkage happens with rlibs, we just needed the metadata (which we
+            // got long ago), so don't bother with anything.
+            CrateType::Rlib => Linkage::NotLinked,
+        };
 
     let mut unavailable_as_static = Vec::new();
 
@@ -165,7 +162,9 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
 
     let all_dylibs = || {
         tcx.crates(()).iter().filter(|&&cnum| {
-            !tcx.dep_kind(cnum).macros_only() && tcx.used_crate_source(cnum).dylib.is_some()
+            !tcx.dep_kind(cnum).macros_only()
+                && (tcx.used_crate_source(cnum).dylib.is_some()
+                    || tcx.used_crate_source(cnum).sdylib_interface.is_some())
         })
     };
 
@@ -273,7 +272,7 @@ fn calculate_type(tcx: TyCtxt<'_>, ty: CrateType) -> DependencyList {
         match *kind {
             Linkage::NotLinked | Linkage::IncludedFromDylib => {}
             Linkage::Static if src.rlib.is_some() => continue,
-            Linkage::Dynamic if src.dylib.is_some() => continue,
+            Linkage::Dynamic if src.dylib.is_some() || src.sdylib_interface.is_some() => continue,
             kind => {
                 let kind = match kind {
                     Linkage::Static => "rlib",
diff --git a/compiler/rustc_metadata/src/locator.rs b/compiler/rustc_metadata/src/locator.rs
index f0a898d678c..10123cb9a9d 100644
--- a/compiler/rustc_metadata/src/locator.rs
+++ b/compiler/rustc_metadata/src/locator.rs
@@ -220,7 +220,7 @@ use std::{cmp, fmt};
 
 use rustc_data_structures::fx::{FxHashSet, FxIndexMap};
 use rustc_data_structures::memmap::Mmap;
-use rustc_data_structures::owned_slice::slice_owned;
+use rustc_data_structures::owned_slice::{OwnedSlice, slice_owned};
 use rustc_data_structures::svh::Svh;
 use rustc_errors::{DiagArgValue, IntoDiagArg};
 use rustc_fs_util::try_canonicalize;
@@ -231,6 +231,7 @@ use rustc_session::search_paths::PathKind;
 use rustc_session::utils::CanonicalizedPath;
 use rustc_span::{Span, Symbol};
 use rustc_target::spec::{Target, TargetTuple};
+use tempfile::Builder as TempFileBuilder;
 use tracing::{debug, info};
 
 use crate::creader::{Library, MetadataLoader};
@@ -277,6 +278,7 @@ pub(crate) enum CrateFlavor {
     Rlib,
     Rmeta,
     Dylib,
+    SDylib,
 }
 
 impl fmt::Display for CrateFlavor {
@@ -285,6 +287,7 @@ impl fmt::Display for CrateFlavor {
             CrateFlavor::Rlib => "rlib",
             CrateFlavor::Rmeta => "rmeta",
             CrateFlavor::Dylib => "dylib",
+            CrateFlavor::SDylib => "sdylib",
         })
     }
 }
@@ -295,6 +298,7 @@ impl IntoDiagArg for CrateFlavor {
             CrateFlavor::Rlib => DiagArgValue::Str(Cow::Borrowed("rlib")),
             CrateFlavor::Rmeta => DiagArgValue::Str(Cow::Borrowed("rmeta")),
             CrateFlavor::Dylib => DiagArgValue::Str(Cow::Borrowed("dylib")),
+            CrateFlavor::SDylib => DiagArgValue::Str(Cow::Borrowed("sdylib")),
         }
     }
 }
@@ -379,14 +383,18 @@ impl<'a> CrateLocator<'a> {
             &format!("{}{}{}", self.target.dll_prefix, self.crate_name, extra_prefix);
         let staticlib_prefix =
             &format!("{}{}{}", self.target.staticlib_prefix, self.crate_name, extra_prefix);
+        let interface_prefix = rmeta_prefix;
 
         let rmeta_suffix = ".rmeta";
         let rlib_suffix = ".rlib";
         let dylib_suffix = &self.target.dll_suffix;
         let staticlib_suffix = &self.target.staticlib_suffix;
+        let interface_suffix = ".rs";
 
-        let mut candidates: FxIndexMap<_, (FxIndexMap<_, _>, FxIndexMap<_, _>, FxIndexMap<_, _>)> =
-            Default::default();
+        let mut candidates: FxIndexMap<
+            _,
+            (FxIndexMap<_, _>, FxIndexMap<_, _>, FxIndexMap<_, _>, FxIndexMap<_, _>),
+        > = Default::default();
 
         // First, find all possible candidate rlibs and dylibs purely based on
         // the name of the files themselves. We're trying to match against an
@@ -417,6 +425,7 @@ impl<'a> CrateLocator<'a> {
                 (rlib_prefix.as_str(), rlib_suffix, CrateFlavor::Rlib),
                 (rmeta_prefix.as_str(), rmeta_suffix, CrateFlavor::Rmeta),
                 (dylib_prefix, dylib_suffix, CrateFlavor::Dylib),
+                (interface_prefix, interface_suffix, CrateFlavor::SDylib),
             ] {
                 if prefix == staticlib_prefix && suffix == staticlib_suffix {
                     should_check_staticlibs = false;
@@ -425,7 +434,7 @@ impl<'a> CrateLocator<'a> {
                     for (hash, spf) in matches {
                         info!("lib candidate: {}", spf.path.display());
 
-                        let (rlibs, rmetas, dylibs) =
+                        let (rlibs, rmetas, dylibs, interfaces) =
                             candidates.entry(hash.to_string()).or_default();
                         {
                             // As a perforamnce optimisation we canonicalize the path and skip
@@ -446,6 +455,7 @@ impl<'a> CrateLocator<'a> {
                             CrateFlavor::Rlib => rlibs.insert(path, search_path.kind),
                             CrateFlavor::Rmeta => rmetas.insert(path, search_path.kind),
                             CrateFlavor::Dylib => dylibs.insert(path, search_path.kind),
+                            CrateFlavor::SDylib => interfaces.insert(path, search_path.kind),
                         };
                     }
                 }
@@ -472,8 +482,8 @@ impl<'a> CrateLocator<'a> {
         // libraries corresponds to the crate id and hash criteria that this
         // search is being performed for.
         let mut libraries = FxIndexMap::default();
-        for (_hash, (rlibs, rmetas, dylibs)) in candidates {
-            if let Some((svh, lib)) = self.extract_lib(rlibs, rmetas, dylibs)? {
+        for (_hash, (rlibs, rmetas, dylibs, interfaces)) in candidates {
+            if let Some((svh, lib)) = self.extract_lib(rlibs, rmetas, dylibs, interfaces)? {
                 libraries.insert(svh, lib);
             }
         }
@@ -508,6 +518,7 @@ impl<'a> CrateLocator<'a> {
         rlibs: FxIndexMap<PathBuf, PathKind>,
         rmetas: FxIndexMap<PathBuf, PathKind>,
         dylibs: FxIndexMap<PathBuf, PathKind>,
+        interfaces: FxIndexMap<PathBuf, PathKind>,
     ) -> Result<Option<(Svh, Library)>, CrateError> {
         let mut slot = None;
         // Order here matters, rmeta should come first.
@@ -515,12 +526,17 @@ impl<'a> CrateLocator<'a> {
         // Make sure there's at most one rlib and at most one dylib.
         //
         // See comment in `extract_one` below.
-        let source = CrateSource {
-            rmeta: self.extract_one(rmetas, CrateFlavor::Rmeta, &mut slot)?,
-            rlib: self.extract_one(rlibs, CrateFlavor::Rlib, &mut slot)?,
-            dylib: self.extract_one(dylibs, CrateFlavor::Dylib, &mut slot)?,
-        };
-        Ok(slot.map(|(svh, metadata, _)| (svh, Library { source, metadata })))
+        let rmeta = self.extract_one(rmetas, CrateFlavor::Rmeta, &mut slot)?;
+        let rlib = self.extract_one(rlibs, CrateFlavor::Rlib, &mut slot)?;
+        let sdylib_interface = self.extract_one(interfaces, CrateFlavor::SDylib, &mut slot)?;
+        let dylib = self.extract_one(dylibs, CrateFlavor::Dylib, &mut slot)?;
+
+        if sdylib_interface.is_some() && dylib.is_none() {
+            return Err(CrateError::FullMetadataNotFound(self.crate_name, CrateFlavor::SDylib));
+        }
+
+        let source = CrateSource { rmeta, rlib, dylib, sdylib_interface };
+        Ok(slot.map(|(svh, metadata, _, _)| (svh, Library { source, metadata })))
     }
 
     fn needs_crate_flavor(&self, flavor: CrateFlavor) -> bool {
@@ -550,7 +566,7 @@ impl<'a> CrateLocator<'a> {
         &mut self,
         m: FxIndexMap<PathBuf, PathKind>,
         flavor: CrateFlavor,
-        slot: &mut Option<(Svh, MetadataBlob, PathBuf)>,
+        slot: &mut Option<(Svh, MetadataBlob, PathBuf, CrateFlavor)>,
     ) -> Result<Option<(PathBuf, PathKind)>, CrateError> {
         // If we are producing an rlib, and we've already loaded metadata, then
         // we should not attempt to discover further crate sources (unless we're
@@ -586,6 +602,7 @@ impl<'a> CrateLocator<'a> {
                 &lib,
                 self.metadata_loader,
                 self.cfg_version,
+                Some(self.crate_name),
             ) {
                 Ok(blob) => {
                     if let Some(h) = self.crate_matches(&blob, &lib) {
@@ -610,6 +627,11 @@ impl<'a> CrateLocator<'a> {
                 }
                 Err(MetadataError::LoadFailure(err)) => {
                     info!("no metadata found: {}", err);
+                    // Metadata was loaded from interface file earlier.
+                    if let Some((.., CrateFlavor::SDylib)) = slot {
+                        ret = Some((lib, kind));
+                        continue;
+                    }
                     // The file was present and created by the same compiler version, but we
                     // couldn't load it for some reason. Give a hard error instead of silently
                     // ignoring it, but only if we would have given an error anyway.
@@ -679,7 +701,7 @@ impl<'a> CrateLocator<'a> {
                     return Err(CrateError::FullMetadataNotFound(self.crate_name, flavor));
                 }
             } else {
-                *slot = Some((hash, metadata, lib.clone()));
+                *slot = Some((hash, metadata, lib.clone(), flavor));
             }
             ret = Some((lib, kind));
         }
@@ -736,6 +758,7 @@ impl<'a> CrateLocator<'a> {
         let mut rlibs = FxIndexMap::default();
         let mut rmetas = FxIndexMap::default();
         let mut dylibs = FxIndexMap::default();
+        let mut sdylib_interfaces = FxIndexMap::default();
         for loc in &self.exact_paths {
             let loc_canon = loc.canonicalized();
             let loc_orig = loc.original();
@@ -763,6 +786,9 @@ impl<'a> CrateLocator<'a> {
                     rmetas.insert(loc_canon.clone(), PathKind::ExternFlag);
                     continue;
                 }
+                if file.ends_with(".rs") {
+                    sdylib_interfaces.insert(loc_canon.clone(), PathKind::ExternFlag);
+                }
             }
             let dll_prefix = self.target.dll_prefix.as_ref();
             let dll_suffix = self.target.dll_suffix.as_ref();
@@ -776,7 +802,8 @@ impl<'a> CrateLocator<'a> {
         }
 
         // Extract the dylib/rlib/rmeta triple.
-        self.extract_lib(rlibs, rmetas, dylibs).map(|opt| opt.map(|(_, lib)| lib))
+        self.extract_lib(rlibs, rmetas, dylibs, sdylib_interfaces)
+            .map(|opt| opt.map(|(_, lib)| lib))
     }
 
     pub(crate) fn into_error(self, dep_root: Option<CratePaths>) -> CrateError {
@@ -797,6 +824,7 @@ fn get_metadata_section<'p>(
     filename: &'p Path,
     loader: &dyn MetadataLoader,
     cfg_version: &'static str,
+    crate_name: Option<Symbol>,
 ) -> Result<MetadataBlob, MetadataError<'p>> {
     if !filename.exists() {
         return Err(MetadataError::NotPresent(filename));
@@ -805,6 +833,55 @@ fn get_metadata_section<'p>(
         CrateFlavor::Rlib => {
             loader.get_rlib_metadata(target, filename).map_err(MetadataError::LoadFailure)?
         }
+        CrateFlavor::SDylib => {
+            let compiler = std::env::current_exe().map_err(|_err| {
+                MetadataError::LoadFailure(
+                    "couldn't obtain current compiler binary when loading sdylib interface"
+                        .to_string(),
+                )
+            })?;
+
+            let tmp_path = match TempFileBuilder::new().prefix("rustc").tempdir() {
+                Ok(tmp_path) => tmp_path,
+                Err(error) => {
+                    return Err(MetadataError::LoadFailure(format!(
+                        "couldn't create a temp dir: {}",
+                        error
+                    )));
+                }
+            };
+
+            let crate_name = crate_name.unwrap();
+            debug!("compiling {}", filename.display());
+            // FIXME: This will need to be done either within the current compiler session or
+            // as a separate compiler session in the same process.
+            let res = std::process::Command::new(compiler)
+                .arg(&filename)
+                .arg("--emit=metadata")
+                .arg(format!("--crate-name={}", crate_name))
+                .arg(format!("--out-dir={}", tmp_path.path().display()))
+                .arg("-Zbuild-sdylib-interface")
+                .output()
+                .map_err(|err| {
+                    MetadataError::LoadFailure(format!("couldn't compile interface: {}", err))
+                })?;
+
+            if !res.status.success() {
+                return Err(MetadataError::LoadFailure(format!(
+                    "couldn't compile interface: {}",
+                    std::str::from_utf8(&res.stderr).unwrap_or_default()
+                )));
+            }
+
+            // Load interface metadata instead of crate metadata.
+            let interface_metadata_name = format!("lib{}.rmeta", crate_name);
+            let rmeta_file = tmp_path.path().join(interface_metadata_name);
+            debug!("loading interface metadata from {}", rmeta_file.display());
+            let rmeta = get_rmeta_metadata_section(&rmeta_file)?;
+            let _ = std::fs::remove_file(rmeta_file);
+
+            rmeta
+        }
         CrateFlavor::Dylib => {
             let buf =
                 loader.get_dylib_metadata(target, filename).map_err(MetadataError::LoadFailure)?;
@@ -834,24 +911,7 @@ fn get_metadata_section<'p>(
             // Header is okay -> inflate the actual metadata
             buf.slice(|buf| &buf[data_start..(data_start + metadata_len)])
         }
-        CrateFlavor::Rmeta => {
-            // mmap the file, because only a small fraction of it is read.
-            let file = std::fs::File::open(filename).map_err(|_| {
-                MetadataError::LoadFailure(format!(
-                    "failed to open rmeta metadata: '{}'",
-                    filename.display()
-                ))
-            })?;
-            let mmap = unsafe { Mmap::map(file) };
-            let mmap = mmap.map_err(|_| {
-                MetadataError::LoadFailure(format!(
-                    "failed to mmap rmeta metadata: '{}'",
-                    filename.display()
-                ))
-            })?;
-
-            slice_owned(mmap, Deref::deref)
-        }
+        CrateFlavor::Rmeta => get_rmeta_metadata_section(filename)?,
     };
     let Ok(blob) = MetadataBlob::new(raw_bytes) else {
         return Err(MetadataError::LoadFailure(format!(
@@ -877,6 +937,25 @@ fn get_metadata_section<'p>(
     }
 }
 
+fn get_rmeta_metadata_section<'a, 'p>(filename: &'p Path) -> Result<OwnedSlice, MetadataError<'a>> {
+    // mmap the file, because only a small fraction of it is read.
+    let file = std::fs::File::open(filename).map_err(|_| {
+        MetadataError::LoadFailure(format!(
+            "failed to open rmeta metadata: '{}'",
+            filename.display()
+        ))
+    })?;
+    let mmap = unsafe { Mmap::map(file) };
+    let mmap = mmap.map_err(|_| {
+        MetadataError::LoadFailure(format!(
+            "failed to mmap rmeta metadata: '{}'",
+            filename.display()
+        ))
+    })?;
+
+    Ok(slice_owned(mmap, Deref::deref))
+}
+
 /// A diagnostic function for dumping crate metadata to an output stream.
 pub fn list_file_metadata(
     target: &Target,
@@ -887,7 +966,7 @@ pub fn list_file_metadata(
     cfg_version: &'static str,
 ) -> IoResult<()> {
     let flavor = get_flavor_from_path(path);
-    match get_metadata_section(target, flavor, path, metadata_loader, cfg_version) {
+    match get_metadata_section(target, flavor, path, metadata_loader, cfg_version, None) {
         Ok(metadata) => metadata.list_crate_metadata(out, ls_kinds),
         Err(msg) => write!(out, "{msg}\n"),
     }
diff --git a/compiler/rustc_metadata/src/rmeta/decoder.rs b/compiler/rustc_metadata/src/rmeta/decoder.rs
index 3c2245347f9..bd813cadedc 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder.rs
@@ -1489,6 +1489,17 @@ impl<'a> CrateMetadataRef<'a> {
         tcx.arena.alloc_from_iter(self.root.lang_items_missing.decode(self))
     }
 
+    fn get_exportable_items(self) -> impl Iterator<Item = DefId> {
+        self.root.exportable_items.decode(self).map(move |index| self.local_def_id(index))
+    }
+
+    fn get_stable_order_of_exportable_impls(self) -> impl Iterator<Item = (DefId, usize)> {
+        self.root
+            .stable_order_of_exportable_impls
+            .decode(self)
+            .map(move |v| (self.local_def_id(v.0), v.1))
+    }
+
     fn exported_symbols<'tcx>(
         self,
         tcx: TyCtxt<'tcx>,
diff --git a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
index ecc2dcc5318..76bae39ef8c 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
@@ -406,6 +406,8 @@ provide! { tcx, def_id, other, cdata,
     used_crate_source => { Arc::clone(&cdata.source) }
     debugger_visualizers => { cdata.get_debugger_visualizers() }
 
+    exportable_items => { tcx.arena.alloc_from_iter(cdata.get_exportable_items()) }
+    stable_order_of_exportable_impls => { tcx.arena.alloc(cdata.get_stable_order_of_exportable_impls().collect()) }
     exported_symbols => {
         let syms = cdata.exported_symbols(tcx);
 
diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs
index 3ea61d1b40a..bbff570d6c6 100644
--- a/compiler/rustc_metadata/src/rmeta/encoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/encoder.rs
@@ -673,6 +673,11 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
         let debugger_visualizers =
             stat!("debugger-visualizers", || self.encode_debugger_visualizers());
 
+        let exportable_items = stat!("exportable-items", || self.encode_exportable_items());
+
+        let stable_order_of_exportable_impls =
+            stat!("exportable-items", || self.encode_stable_order_of_exportable_impls());
+
         // Encode exported symbols info. This is prefetched in `encode_metadata`.
         let exported_symbols = stat!("exported-symbols", || {
             self.encode_exported_symbols(tcx.exported_symbols(LOCAL_CRATE))
@@ -740,6 +745,8 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
                 traits,
                 impls,
                 incoherent_impls,
+                exportable_items,
+                stable_order_of_exportable_impls,
                 exported_symbols,
                 interpret_alloc_index,
                 tables,
@@ -2149,6 +2156,20 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
         self.lazy_array(&all_impls)
     }
 
+    fn encode_exportable_items(&mut self) -> LazyArray<DefIndex> {
+        empty_proc_macro!(self);
+        self.lazy_array(self.tcx.exportable_items(LOCAL_CRATE).iter().map(|def_id| def_id.index))
+    }
+
+    fn encode_stable_order_of_exportable_impls(&mut self) -> LazyArray<(DefIndex, usize)> {
+        empty_proc_macro!(self);
+        let stable_order_of_exportable_impls =
+            self.tcx.stable_order_of_exportable_impls(LOCAL_CRATE);
+        self.lazy_array(
+            stable_order_of_exportable_impls.iter().map(|(def_id, idx)| (def_id.index, *idx)),
+        )
+    }
+
     // Encodes all symbols exported from this crate into the metadata.
     //
     // This pass is seeded off the reachability list calculated in the
diff --git a/compiler/rustc_metadata/src/rmeta/mod.rs b/compiler/rustc_metadata/src/rmeta/mod.rs
index 5aa81f41b7b..c86cf567283 100644
--- a/compiler/rustc_metadata/src/rmeta/mod.rs
+++ b/compiler/rustc_metadata/src/rmeta/mod.rs
@@ -280,6 +280,8 @@ pub(crate) struct CrateRoot {
     tables: LazyTables,
     debugger_visualizers: LazyArray<DebuggerVisualizerFile>,
 
+    exportable_items: LazyArray<DefIndex>,
+    stable_order_of_exportable_impls: LazyArray<(DefIndex, usize)>,
     exported_symbols: LazyArray<(ExportedSymbol<'static>, SymbolExportInfo)>,
 
     syntax_contexts: SyntaxContextTable,
diff --git a/compiler/rustc_middle/src/arena.rs b/compiler/rustc_middle/src/arena.rs
index 2dcb0de92b7..a0f45974089 100644
--- a/compiler/rustc_middle/src/arena.rs
+++ b/compiler/rustc_middle/src/arena.rs
@@ -91,6 +91,8 @@ macro_rules! arena_types {
             [] autodiff_item: rustc_ast::expand::autodiff_attrs::AutoDiffItem,
             [] ordered_name_set: rustc_data_structures::fx::FxIndexSet<rustc_span::Symbol>,
             [] valtree: rustc_middle::ty::ValTreeKind<'tcx>,
+            [] stable_order_of_exportable_impls:
+                rustc_data_structures::fx::FxIndexMap<rustc_hir::def_id::DefId, usize>,
 
             // Note that this deliberately duplicates items in the `rustc_hir::arena`,
             // since we need to allocate this type on both the `rustc_hir` arena
diff --git a/compiler/rustc_middle/src/query/mod.rs b/compiler/rustc_middle/src/query/mod.rs
index 88f4c4ae4d3..6af9d4aae30 100644
--- a/compiler/rustc_middle/src/query/mod.rs
+++ b/compiler/rustc_middle/src/query/mod.rs
@@ -2239,6 +2239,16 @@ rustc_queries! {
         separate_provide_extern
     }
 
+    query stable_order_of_exportable_impls(_: CrateNum) -> &'tcx FxIndexMap<DefId, usize> {
+        desc { "fetching the stable impl's order" }
+        separate_provide_extern
+    }
+
+    query exportable_items(_: CrateNum) -> &'tcx [DefId] {
+        desc { "fetching all exportable items in a crate" }
+        separate_provide_extern
+    }
+
     /// The list of symbols exported from the given crate.
     ///
     /// - All names contained in `exported_symbols(cnum)` are guaranteed to
diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs
index 0f7f8527088..d660234618e 100644
--- a/compiler/rustc_middle/src/ty/context.rs
+++ b/compiler/rustc_middle/src/ty/context.rs
@@ -1828,9 +1828,10 @@ impl<'tcx> TyCtxt<'tcx> {
         self.crate_types()
             .iter()
             .map(|ty| match *ty {
-                CrateType::Executable | CrateType::Staticlib | CrateType::Cdylib => {
-                    MetadataKind::None
-                }
+                CrateType::Executable
+                | CrateType::Staticlib
+                | CrateType::Cdylib
+                | CrateType::Sdylib => MetadataKind::None,
                 CrateType::Rlib => MetadataKind::Uncompressed,
                 CrateType::Dylib | CrateType::ProcMacro => MetadataKind::Compressed,
             })
@@ -2133,7 +2134,8 @@ impl<'tcx> TyCtxt<'tcx> {
                 CrateType::Executable
                 | CrateType::Staticlib
                 | CrateType::ProcMacro
-                | CrateType::Cdylib => false,
+                | CrateType::Cdylib
+                | CrateType::Sdylib => false,
 
                 // FIXME rust-lang/rust#64319, rust-lang/rust#64872:
                 // We want to block export of generics from dylibs,
@@ -3315,6 +3317,10 @@ impl<'tcx> TyCtxt<'tcx> {
             && self.impl_trait_header(def_id).unwrap().constness == hir::Constness::Const
     }
 
+    pub fn is_sdylib_interface_build(self) -> bool {
+        self.sess.opts.unstable_opts.build_sdylib_interface
+    }
+
     pub fn intrinsic(self, def_id: impl IntoQueryParam<DefId> + Copy) -> Option<ty::IntrinsicDef> {
         match self.def_kind(def_id) {
             DefKind::Fn | DefKind::AssocFn => {}
diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs
index b13e33718b2..b4ef82f6d42 100644
--- a/compiler/rustc_middle/src/ty/mod.rs
+++ b/compiler/rustc_middle/src/ty/mod.rs
@@ -1982,6 +1982,10 @@ impl<'tcx> TyCtxt<'tcx> {
         None
     }
 
+    pub fn is_exportable(self, def_id: DefId) -> bool {
+        self.exportable_items(def_id.krate).contains(&def_id)
+    }
+
     /// Check if the given `DefId` is `#\[automatically_derived\]`, *and*
     /// whether it was produced by expanding a builtin derive macro.
     pub fn is_builtin_derived(self, def_id: DefId) -> bool {
diff --git a/compiler/rustc_mir_build/src/builder/mod.rs b/compiler/rustc_mir_build/src/builder/mod.rs
index 59a52ae67cb..9cf051a8760 100644
--- a/compiler/rustc_mir_build/src/builder/mod.rs
+++ b/compiler/rustc_mir_build/src/builder/mod.rs
@@ -998,7 +998,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
             self.source_scope = source_scope;
         }
 
-        if self.tcx.intrinsic(self.def_id).is_some_and(|i| i.must_be_overridden) {
+        if self.tcx.intrinsic(self.def_id).is_some_and(|i| i.must_be_overridden)
+            || self.tcx.is_sdylib_interface_build()
+        {
             let source_info = self.source_info(rustc_span::DUMMY_SP);
             self.cfg.terminate(block, source_info, TerminatorKind::Unreachable);
             self.cfg.start_new_block().unit()
diff --git a/compiler/rustc_passes/messages.ftl b/compiler/rustc_passes/messages.ftl
index 413726ddd82..6d815e510ea 100644
--- a/compiler/rustc_passes/messages.ftl
+++ b/compiler/rustc_passes/messages.ftl
@@ -356,6 +356,8 @@ passes_ignored_derived_impls =
 passes_implied_feature_not_exist =
     feature `{$implied_by}` implying `{$feature}` does not exist
 
+passes_incorrect_crate_type = lang items are not allowed in stable dylibs
+
 passes_incorrect_do_not_recommend_args =
     `#[diagnostic::do_not_recommend]` does not expect any arguments
 
@@ -742,6 +744,23 @@ passes_trait_impl_const_stable =
 passes_transparent_incompatible =
     transparent {$target} cannot have other repr hints
 
+passes_unexportable_adt_with_private_fields = ADT types with private fields are not exportable
+    .note = `{$field_name}` is private
+
+passes_unexportable_fn_abi = only functions with "C" ABI are exportable
+
+passes_unexportable_generic_fn = generic functions are not exportable
+
+passes_unexportable_item = {$descr}'s are not exportable
+
+passes_unexportable_priv_item = private items are not exportable
+    .note = is only usable at visibility `{$vis_descr}`
+
+passes_unexportable_type_in_interface = {$desc} with `#[export_stable]` attribute uses type `{$ty}`, which is not exportable
+    .label = not exportable
+
+passes_unexportable_type_repr = types with unstable layout are not exportable
+
 passes_unknown_external_lang_item =
     unknown external lang item: `{$lang_item}`
 
diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs
index e5b20901c0c..c68f8df49fc 100644
--- a/compiler/rustc_passes/src/check_attr.rs
+++ b/compiler/rustc_passes/src/check_attr.rs
@@ -277,6 +277,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
                             | sym::cfg_attr
                             | sym::cfg_trace
                             | sym::cfg_attr_trace
+                            | sym::export_stable // handled in `check_export`
                             // need to be fixed
                             | sym::cfi_encoding // FIXME(cfi_encoding)
                             | sym::pointee // FIXME(derive_coerce_pointee)
diff --git a/compiler/rustc_passes/src/check_export.rs b/compiler/rustc_passes/src/check_export.rs
new file mode 100644
index 00000000000..2bb698689be
--- /dev/null
+++ b/compiler/rustc_passes/src/check_export.rs
@@ -0,0 +1,398 @@
+use std::iter;
+use std::ops::ControlFlow;
+
+use rustc_abi::ExternAbi;
+use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
+use rustc_hir as hir;
+use rustc_hir::def::DefKind;
+use rustc_hir::def_id::{DefId, LocalDefId};
+use rustc_hir::intravisit::{self, Visitor};
+use rustc_middle::hir::nested_filter;
+use rustc_middle::middle::privacy::{EffectiveVisibility, Level};
+use rustc_middle::query::{LocalCrate, Providers};
+use rustc_middle::ty::{
+    self, Ty, TyCtxt, TypeSuperVisitable, TypeVisitable, TypeVisitor, Visibility,
+};
+use rustc_session::config::CrateType;
+use rustc_span::{Span, sym};
+
+use crate::errors::UnexportableItem;
+
+struct ExportableItemCollector<'tcx> {
+    tcx: TyCtxt<'tcx>,
+    exportable_items: FxIndexSet<DefId>,
+    in_exportable_mod: bool,
+    seen_exportable_in_mod: bool,
+}
+
+impl<'tcx> ExportableItemCollector<'tcx> {
+    fn new(tcx: TyCtxt<'tcx>) -> ExportableItemCollector<'tcx> {
+        ExportableItemCollector {
+            tcx,
+            exportable_items: Default::default(),
+            in_exportable_mod: false,
+            seen_exportable_in_mod: false,
+        }
+    }
+
+    fn report_wrong_site(&self, def_id: LocalDefId) {
+        let def_descr = self.tcx.def_descr(def_id.to_def_id());
+        self.tcx.dcx().emit_err(UnexportableItem::Item {
+            descr: &format!("{}", def_descr),
+            span: self.tcx.def_span(def_id),
+        });
+    }
+
+    fn item_is_exportable(&self, def_id: LocalDefId) -> bool {
+        let has_attr = self.tcx.has_attr(def_id, sym::export_stable);
+        if !self.in_exportable_mod && !has_attr {
+            return false;
+        }
+
+        let visibilities = self.tcx.effective_visibilities(());
+        let is_pub = visibilities.is_directly_public(def_id);
+
+        if has_attr && !is_pub {
+            let vis = visibilities.effective_vis(def_id).cloned().unwrap_or(
+                EffectiveVisibility::from_vis(Visibility::Restricted(
+                    self.tcx.parent_module_from_def_id(def_id).to_local_def_id(),
+                )),
+            );
+            let vis = vis.at_level(Level::Direct);
+            let span = self.tcx.def_span(def_id);
+
+            self.tcx.dcx().emit_err(UnexportableItem::PrivItem {
+                vis_note: span,
+                vis_descr: &vis.to_string(def_id, self.tcx),
+                span,
+            });
+            return false;
+        }
+
+        is_pub && (has_attr || self.in_exportable_mod)
+    }
+
+    fn add_exportable(&mut self, def_id: LocalDefId) {
+        self.seen_exportable_in_mod = true;
+        self.exportable_items.insert(def_id.to_def_id());
+    }
+
+    fn walk_item_with_mod(&mut self, item: &'tcx hir::Item<'tcx>) {
+        let def_id = item.hir_id().owner.def_id;
+        let old_exportable_mod = self.in_exportable_mod;
+        if self.tcx.get_attr(def_id, sym::export_stable).is_some() {
+            self.in_exportable_mod = true;
+        }
+        let old_seen_exportable_in_mod = std::mem::replace(&mut self.seen_exportable_in_mod, false);
+
+        intravisit::walk_item(self, item);
+
+        if self.seen_exportable_in_mod || self.in_exportable_mod {
+            self.exportable_items.insert(def_id.to_def_id());
+        }
+
+        self.seen_exportable_in_mod = old_seen_exportable_in_mod;
+        self.in_exportable_mod = old_exportable_mod;
+    }
+}
+
+impl<'tcx> Visitor<'tcx> for ExportableItemCollector<'tcx> {
+    type NestedFilter = nested_filter::All;
+
+    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
+        self.tcx
+    }
+
+    fn visit_item(&mut self, item: &'tcx hir::Item<'tcx>) {
+        let def_id = item.hir_id().owner.def_id;
+        // Applying #[extern] attribute to modules is simply equivalent to
+        // applying the attribute to every public item within it.
+        match item.kind {
+            hir::ItemKind::Mod(..) => {
+                self.walk_item_with_mod(item);
+                return;
+            }
+            hir::ItemKind::Impl(impl_) if impl_.of_trait.is_none() => {
+                self.walk_item_with_mod(item);
+                return;
+            }
+            _ => {}
+        }
+
+        if !self.item_is_exportable(def_id) {
+            return;
+        }
+
+        match item.kind {
+            hir::ItemKind::Fn { .. }
+            | hir::ItemKind::Struct(..)
+            | hir::ItemKind::Enum(..)
+            | hir::ItemKind::Union(..)
+            | hir::ItemKind::TyAlias(..) => {
+                self.add_exportable(def_id);
+            }
+            hir::ItemKind::Use(path, _) => {
+                for res in &path.res {
+                    // Only local items are exportable.
+                    if let Some(res_id) = res.opt_def_id()
+                        && let Some(res_id) = res_id.as_local()
+                    {
+                        self.add_exportable(res_id);
+                    }
+                }
+            }
+            // handled above
+            hir::ItemKind::Mod(..) => unreachable!(),
+            hir::ItemKind::Impl(impl_) if impl_.of_trait.is_none() => {
+                unreachable!();
+            }
+            _ => self.report_wrong_site(def_id),
+        }
+    }
+
+    fn visit_impl_item(&mut self, item: &'tcx hir::ImplItem<'tcx>) {
+        let def_id = item.hir_id().owner.def_id;
+        if !self.item_is_exportable(def_id) {
+            return;
+        }
+        match item.kind {
+            hir::ImplItemKind::Fn(..) | hir::ImplItemKind::Type(..) => {
+                self.add_exportable(def_id);
+            }
+            _ => self.report_wrong_site(def_id),
+        }
+    }
+
+    fn visit_foreign_item(&mut self, item: &'tcx hir::ForeignItem<'tcx>) {
+        let def_id = item.hir_id().owner.def_id;
+        if !self.item_is_exportable(def_id) {
+            self.report_wrong_site(def_id);
+        }
+    }
+
+    fn visit_trait_item(&mut self, item: &'tcx hir::TraitItem<'tcx>) {
+        let def_id = item.hir_id().owner.def_id;
+        if !self.item_is_exportable(def_id) {
+            self.report_wrong_site(def_id);
+        }
+    }
+}
+
+struct ExportableItemsChecker<'tcx, 'a> {
+    tcx: TyCtxt<'tcx>,
+    exportable_items: &'a FxIndexSet<DefId>,
+    item_id: DefId,
+}
+
+impl<'tcx, 'a> ExportableItemsChecker<'tcx, 'a> {
+    fn check(&mut self) {
+        match self.tcx.def_kind(self.item_id) {
+            DefKind::Fn | DefKind::AssocFn => self.check_fn(),
+            DefKind::Enum | DefKind::Struct | DefKind::Union => self.check_ty(),
+            _ => {}
+        }
+    }
+
+    fn check_fn(&mut self) {
+        let def_id = self.item_id.expect_local();
+        let span = self.tcx.def_span(def_id);
+
+        if self.tcx.generics_of(def_id).requires_monomorphization(self.tcx) {
+            self.tcx.dcx().emit_err(UnexportableItem::GenericFn(span));
+            return;
+        }
+
+        let sig = self.tcx.fn_sig(def_id).instantiate_identity().skip_binder();
+        if !matches!(sig.abi, ExternAbi::C { .. }) {
+            self.tcx.dcx().emit_err(UnexportableItem::FnAbi(span));
+            return;
+        }
+
+        let sig = self
+            .tcx
+            .try_normalize_erasing_regions(ty::TypingEnv::non_body_analysis(self.tcx, def_id), sig)
+            .unwrap_or(sig);
+
+        let hir_id = self.tcx.local_def_id_to_hir_id(def_id);
+        let decl = self.tcx.hir_fn_decl_by_hir_id(hir_id).unwrap();
+
+        for (input_ty, input_hir) in iter::zip(sig.inputs(), decl.inputs) {
+            self.check_nested_types_are_exportable(*input_ty, input_hir.span);
+        }
+
+        if let hir::FnRetTy::Return(ret_hir) = decl.output {
+            self.check_nested_types_are_exportable(sig.output(), ret_hir.span);
+        }
+    }
+
+    fn check_ty(&mut self) {
+        let ty = self.tcx.type_of(self.item_id).skip_binder();
+        if let ty::Adt(adt_def, _) = ty.kind() {
+            if !adt_def.repr().inhibit_struct_field_reordering() {
+                self.tcx
+                    .dcx()
+                    .emit_err(UnexportableItem::TypeRepr(self.tcx.def_span(self.item_id)));
+            }
+
+            // FIXME: support `#[export(unsafe_stable_abi = "hash")]` syntax
+            for variant in adt_def.variants() {
+                for field in &variant.fields {
+                    if !field.vis.is_public() {
+                        self.tcx.dcx().emit_err(UnexportableItem::AdtWithPrivFields {
+                            span: self.tcx.def_span(self.item_id),
+                            vis_note: self.tcx.def_span(field.did),
+                            field_name: field.name.as_str(),
+                        });
+                    }
+                }
+            }
+        }
+    }
+
+    fn check_nested_types_are_exportable(&mut self, ty: Ty<'tcx>, ty_span: Span) {
+        let res = ty.visit_with(self);
+        if let Some(err_cause) = res.break_value() {
+            self.tcx.dcx().emit_err(UnexportableItem::TypeInInterface {
+                span: self.tcx.def_span(self.item_id),
+                desc: self.tcx.def_descr(self.item_id),
+                ty: &format!("{}", err_cause),
+                ty_span,
+            });
+        }
+    }
+}
+
+impl<'tcx, 'a> TypeVisitor<TyCtxt<'tcx>> for ExportableItemsChecker<'tcx, 'a> {
+    type Result = ControlFlow<Ty<'tcx>>;
+
+    fn visit_ty(&mut self, ty: Ty<'tcx>) -> Self::Result {
+        match ty.kind() {
+            ty::Adt(adt_def, _) => {
+                let did = adt_def.did();
+                let exportable = if did.is_local() {
+                    self.exportable_items.contains(&did)
+                } else {
+                    self.tcx.is_exportable(did)
+                };
+                if !exportable {
+                    return ControlFlow::Break(ty);
+                }
+                for variant in adt_def.variants() {
+                    for field in &variant.fields {
+                        let field_ty = self.tcx.type_of(field.did).instantiate_identity();
+                        field_ty.visit_with(self)?;
+                    }
+                }
+
+                return ty.super_visit_with(self);
+            }
+
+            ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Bool | ty::Char | ty::Error(_) => {}
+
+            ty::Array(_, _)
+            | ty::Ref(_, _, _)
+            | ty::Param(_)
+            | ty::Closure(_, _)
+            | ty::Dynamic(_, _, _)
+            | ty::Coroutine(_, _)
+            | ty::Foreign(_)
+            | ty::Str
+            | ty::Tuple(_)
+            | ty::Pat(..)
+            | ty::Slice(_)
+            | ty::RawPtr(_, _)
+            | ty::FnDef(_, _)
+            | ty::FnPtr(_, _)
+            | ty::CoroutineClosure(_, _)
+            | ty::CoroutineWitness(_, _)
+            | ty::Never
+            | ty::UnsafeBinder(_)
+            | ty::Alias(ty::AliasTyKind::Opaque, _) => {
+                return ControlFlow::Break(ty);
+            }
+
+            ty::Alias(..) | ty::Infer(_) | ty::Placeholder(_) | ty::Bound(..) => unreachable!(),
+        }
+        ControlFlow::Continue(())
+    }
+}
+
+/// Exportable items:
+///
+/// 1. Structs/enums/unions with a stable representation (e.g. repr(i32) or repr(C)).
+/// 2. Primitive types.
+/// 3. Non-generic functions with a stable ABI (e.g. extern "C") for which every user
+///    defined type used in the signature is also marked as `#[export]`.
+fn exportable_items_provider_local<'tcx>(tcx: TyCtxt<'tcx>, _: LocalCrate) -> &'tcx [DefId] {
+    if !tcx.crate_types().contains(&CrateType::Sdylib) && !tcx.is_sdylib_interface_build() {
+        return &[];
+    }
+
+    let mut visitor = ExportableItemCollector::new(tcx);
+    tcx.hir_walk_toplevel_module(&mut visitor);
+    let exportable_items = visitor.exportable_items;
+    for item_id in exportable_items.iter() {
+        let mut validator =
+            ExportableItemsChecker { tcx, exportable_items: &exportable_items, item_id: *item_id };
+        validator.check();
+    }
+
+    tcx.arena.alloc_from_iter(exportable_items.into_iter())
+}
+
+struct ImplsOrderVisitor<'tcx> {
+    tcx: TyCtxt<'tcx>,
+    order: FxIndexMap<DefId, usize>,
+}
+
+impl<'tcx> ImplsOrderVisitor<'tcx> {
+    fn new(tcx: TyCtxt<'tcx>) -> ImplsOrderVisitor<'tcx> {
+        ImplsOrderVisitor { tcx, order: Default::default() }
+    }
+}
+
+impl<'tcx> Visitor<'tcx> for ImplsOrderVisitor<'tcx> {
+    type NestedFilter = nested_filter::All;
+
+    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
+        self.tcx
+    }
+
+    fn visit_item(&mut self, item: &'tcx hir::Item<'tcx>) {
+        if let hir::ItemKind::Impl(impl_) = item.kind
+            && impl_.of_trait.is_none()
+            && self.tcx.is_exportable(item.owner_id.def_id.to_def_id())
+        {
+            self.order.insert(item.owner_id.def_id.to_def_id(), self.order.len());
+        }
+        intravisit::walk_item(self, item);
+    }
+}
+
+/// During symbol mangling rustc uses a special index to distinguish between two impls of
+/// the same type in the same module(See `DisambiguatedDefPathData`). For exportable items
+/// we cannot use the current approach because it is dependent on the compiler's
+/// implementation.
+///
+/// In order to make disambiguation independent of the compiler version we can assign an
+/// id to each impl according to the relative order of elements in the source code.
+fn stable_order_of_exportable_impls<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    _: LocalCrate,
+) -> &'tcx FxIndexMap<DefId, usize> {
+    if !tcx.crate_types().contains(&CrateType::Sdylib) && !tcx.is_sdylib_interface_build() {
+        return tcx.arena.alloc(FxIndexMap::<DefId, usize>::default());
+    }
+
+    let mut vis = ImplsOrderVisitor::new(tcx);
+    tcx.hir_walk_toplevel_module(&mut vis);
+    tcx.arena.alloc(vis.order)
+}
+
+pub(crate) fn provide(providers: &mut Providers) {
+    *providers = Providers {
+        exportable_items: exportable_items_provider_local,
+        stable_order_of_exportable_impls,
+        ..*providers
+    };
+}
diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs
index b1b4b9ee927..00682a9c7a7 100644
--- a/compiler/rustc_passes/src/errors.rs
+++ b/compiler/rustc_passes/src/errors.rs
@@ -1422,6 +1422,13 @@ pub(crate) struct IncorrectTarget<'a> {
     pub at_least: bool,
 }
 
+#[derive(Diagnostic)]
+#[diag(passes_incorrect_crate_type)]
+pub(crate) struct IncorrectCrateType {
+    #[primary_span]
+    pub span: Span,
+}
+
 #[derive(LintDiagnostic)]
 #[diag(passes_useless_assignment)]
 pub(crate) struct UselessAssignment<'a> {
@@ -1919,3 +1926,50 @@ pub(crate) struct UnsupportedAttributesInWhere {
     #[primary_span]
     pub span: MultiSpan,
 }
+
+#[derive(Diagnostic)]
+pub(crate) enum UnexportableItem<'a> {
+    #[diag(passes_unexportable_item)]
+    Item {
+        #[primary_span]
+        span: Span,
+        descr: &'a str,
+    },
+
+    #[diag(passes_unexportable_generic_fn)]
+    GenericFn(#[primary_span] Span),
+
+    #[diag(passes_unexportable_fn_abi)]
+    FnAbi(#[primary_span] Span),
+
+    #[diag(passes_unexportable_type_repr)]
+    TypeRepr(#[primary_span] Span),
+
+    #[diag(passes_unexportable_type_in_interface)]
+    TypeInInterface {
+        #[primary_span]
+        span: Span,
+        desc: &'a str,
+        ty: &'a str,
+        #[label]
+        ty_span: Span,
+    },
+
+    #[diag(passes_unexportable_priv_item)]
+    PrivItem {
+        #[primary_span]
+        span: Span,
+        #[note]
+        vis_note: Span,
+        vis_descr: &'a str,
+    },
+
+    #[diag(passes_unexportable_adt_with_private_fields)]
+    AdtWithPrivFields {
+        #[primary_span]
+        span: Span,
+        #[note]
+        vis_note: Span,
+        field_name: &'a str,
+    },
+}
diff --git a/compiler/rustc_passes/src/lang_items.rs b/compiler/rustc_passes/src/lang_items.rs
index 664bd4ad0a2..275714c2d0e 100644
--- a/compiler/rustc_passes/src/lang_items.rs
+++ b/compiler/rustc_passes/src/lang_items.rs
@@ -19,7 +19,8 @@ use rustc_session::cstore::ExternCrate;
 use rustc_span::Span;
 
 use crate::errors::{
-    DuplicateLangItem, IncorrectTarget, LangItemOnIncorrectTarget, UnknownLangItem,
+    DuplicateLangItem, IncorrectCrateType, IncorrectTarget, LangItemOnIncorrectTarget,
+    UnknownLangItem,
 };
 use crate::weak_lang_items;
 
@@ -236,6 +237,10 @@ impl<'ast, 'tcx> LanguageItemCollector<'ast, 'tcx> {
             }
         }
 
+        if self.tcx.crate_types().contains(&rustc_session::config::CrateType::Sdylib) {
+            self.tcx.dcx().emit_err(IncorrectCrateType { span: attr_span });
+        }
+
         self.collect_item(lang_item, item_def_id.to_def_id(), Some(item_span));
     }
 }
diff --git a/compiler/rustc_passes/src/lib.rs b/compiler/rustc_passes/src/lib.rs
index 424bce9d4d4..001725e2882 100644
--- a/compiler/rustc_passes/src/lib.rs
+++ b/compiler/rustc_passes/src/lib.rs
@@ -19,6 +19,7 @@ use rustc_middle::query::Providers;
 
 pub mod abi_test;
 mod check_attr;
+mod check_export;
 pub mod dead;
 mod debugger_visualizer;
 mod diagnostic_items;
@@ -54,4 +55,5 @@ pub fn provide(providers: &mut Providers) {
     reachable::provide(providers);
     stability::provide(providers);
     upvars::provide(providers);
+    check_export::provide(providers);
 }
diff --git a/compiler/rustc_passes/src/reachable.rs b/compiler/rustc_passes/src/reachable.rs
index 321e5729b72..f0e8fa986fe 100644
--- a/compiler/rustc_passes/src/reachable.rs
+++ b/compiler/rustc_passes/src/reachable.rs
@@ -435,10 +435,12 @@ fn has_custom_linkage(tcx: TyCtxt<'_>, def_id: LocalDefId) -> bool {
 fn reachable_set(tcx: TyCtxt<'_>, (): ()) -> LocalDefIdSet {
     let effective_visibilities = &tcx.effective_visibilities(());
 
-    let any_library = tcx
-        .crate_types()
-        .iter()
-        .any(|ty| *ty == CrateType::Rlib || *ty == CrateType::Dylib || *ty == CrateType::ProcMacro);
+    let any_library = tcx.crate_types().iter().any(|ty| {
+        *ty == CrateType::Rlib
+            || *ty == CrateType::Dylib
+            || *ty == CrateType::ProcMacro
+            || *ty == CrateType::Sdylib
+    });
     let mut reachable_context = ReachableContext {
         tcx,
         maybe_typeck_results: None,
diff --git a/compiler/rustc_passes/src/weak_lang_items.rs b/compiler/rustc_passes/src/weak_lang_items.rs
index 701f500e4f6..93d164e7d01 100644
--- a/compiler/rustc_passes/src/weak_lang_items.rs
+++ b/compiler/rustc_passes/src/weak_lang_items.rs
@@ -67,7 +67,8 @@ fn verify(tcx: TyCtxt<'_>, items: &lang_items::LanguageItems) {
         | CrateType::ProcMacro
         | CrateType::Cdylib
         | CrateType::Executable
-        | CrateType::Staticlib => true,
+        | CrateType::Staticlib
+        | CrateType::Sdylib => true,
         CrateType::Rlib => false,
     });
     if !needs_check {
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index e2d36f6a4e2..a9d9236d318 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -1169,6 +1169,10 @@ impl OutputFilenames {
             .unwrap_or_else(|| OutFileName::Real(self.output_path(flavor)))
     }
 
+    pub fn interface_path(&self) -> PathBuf {
+        self.out_directory.join(format!("lib{}.rs", self.crate_stem))
+    }
+
     /// Gets the output path where a compilation artifact of the given type
     /// should be placed on disk.
     fn output_path(&self, flavor: OutputType) -> PathBuf {
@@ -1452,13 +1456,17 @@ pub enum CrateType {
     Staticlib,
     Cdylib,
     ProcMacro,
+    Sdylib,
 }
 
 impl CrateType {
     pub fn has_metadata(self) -> bool {
         match self {
             CrateType::Rlib | CrateType::Dylib | CrateType::ProcMacro => true,
-            CrateType::Executable | CrateType::Cdylib | CrateType::Staticlib => false,
+            CrateType::Executable
+            | CrateType::Cdylib
+            | CrateType::Staticlib
+            | CrateType::Sdylib => false,
         }
     }
 }
@@ -2818,6 +2826,7 @@ pub fn parse_crate_types_from_list(list_list: Vec<String>) -> Result<Vec<CrateTy
                 "cdylib" => CrateType::Cdylib,
                 "bin" => CrateType::Executable,
                 "proc-macro" => CrateType::ProcMacro,
+                "sdylib" => CrateType::Sdylib,
                 _ => {
                     return Err(format!(
                         "unknown crate type: `{part}`, expected one of: \
@@ -2915,6 +2924,7 @@ impl fmt::Display for CrateType {
             CrateType::Staticlib => "staticlib".fmt(f),
             CrateType::Cdylib => "cdylib".fmt(f),
             CrateType::ProcMacro => "proc-macro".fmt(f),
+            CrateType::Sdylib => "sdylib".fmt(f),
         }
     }
 }
diff --git a/compiler/rustc_session/src/cstore.rs b/compiler/rustc_session/src/cstore.rs
index c8a5c22ad12..4cfc745dec2 100644
--- a/compiler/rustc_session/src/cstore.rs
+++ b/compiler/rustc_session/src/cstore.rs
@@ -27,6 +27,7 @@ pub struct CrateSource {
     pub dylib: Option<(PathBuf, PathKind)>,
     pub rlib: Option<(PathBuf, PathKind)>,
     pub rmeta: Option<(PathBuf, PathKind)>,
+    pub sdylib_interface: Option<(PathBuf, PathKind)>,
 }
 
 impl CrateSource {
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 5f4695fb184..440e8f808c7 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -2113,6 +2113,8 @@ options! {
         "emit noalias metadata for box (default: yes)"),
     branch_protection: Option<BranchProtection> = (None, parse_branch_protection, [TRACKED],
         "set options for branch target identification and pointer authentication on AArch64"),
+    build_sdylib_interface: bool = (false, parse_bool, [UNTRACKED],
+        "whether the stable interface is being built"),
     cf_protection: CFProtection = (CFProtection::None, parse_cfprotection, [TRACKED],
         "instrument control-flow architecture protection"),
     check_cfg_all_expected: bool = (false, parse_bool, [UNTRACKED],
diff --git a/compiler/rustc_session/src/output.rs b/compiler/rustc_session/src/output.rs
index 46dae9144cd..cba70b5bd5d 100644
--- a/compiler/rustc_session/src/output.rs
+++ b/compiler/rustc_session/src/output.rs
@@ -98,7 +98,7 @@ pub fn filename_for_input(
         CrateType::Rlib => {
             OutFileName::Real(outputs.out_directory.join(&format!("lib{libname}.rlib")))
         }
-        CrateType::Cdylib | CrateType::ProcMacro | CrateType::Dylib => {
+        CrateType::Cdylib | CrateType::ProcMacro | CrateType::Dylib | CrateType::Sdylib => {
             let (prefix, suffix) = (&sess.target.dll_prefix, &sess.target.dll_suffix);
             OutFileName::Real(outputs.out_directory.join(&format!("{prefix}{libname}{suffix}")))
         }
@@ -167,6 +167,7 @@ pub const CRATE_TYPES: &[(Symbol, CrateType)] = &[
     (sym::staticlib, CrateType::Staticlib),
     (sym::proc_dash_macro, CrateType::ProcMacro),
     (sym::bin, CrateType::Executable),
+    (sym::sdylib, CrateType::Sdylib),
 ];
 
 pub fn categorize_crate_type(s: Symbol) -> Option<CrateType> {
@@ -187,6 +188,11 @@ pub fn collect_crate_types(session: &Session, attrs: &[ast::Attribute]) -> Vec<C
         return vec![CrateType::Executable];
     }
 
+    // Shadow `sdylib` crate type in interface build.
+    if session.opts.unstable_opts.build_sdylib_interface {
+        return vec![CrateType::Rlib];
+    }
+
     // Only check command line flags if present. If no types are specified by
     // command line, then reuse the empty `base` Vec to hold the types that
     // will be found in crate attributes.
diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs
index 3912c7dc7d6..31d129c3465 100644
--- a/compiler/rustc_span/src/symbol.rs
+++ b/compiler/rustc_span/src/symbol.rs
@@ -914,6 +914,7 @@ symbols! {
         explicit_generic_args_with_impl_trait,
         explicit_tail_calls,
         export_name,
+        export_stable,
         expr,
         expr_2021,
         expr_fragment_specifier_2024,
@@ -1878,6 +1879,7 @@ symbols! {
         saturating_add,
         saturating_div,
         saturating_sub,
+        sdylib,
         search_unbox,
         select_unpredictable,
         self_in_typedefs,
diff --git a/compiler/rustc_symbol_mangling/src/export.rs b/compiler/rustc_symbol_mangling/src/export.rs
new file mode 100644
index 00000000000..770401fc8cf
--- /dev/null
+++ b/compiler/rustc_symbol_mangling/src/export.rs
@@ -0,0 +1,181 @@
+use std::assert_matches::debug_assert_matches;
+
+use rustc_abi::IntegerType;
+use rustc_data_structures::stable_hasher::StableHasher;
+use rustc_hashes::Hash128;
+use rustc_hir::def::DefKind;
+use rustc_middle::ty::{self, Instance, Ty, TyCtxt};
+use rustc_span::symbol::{Symbol, sym};
+
+trait AbiHashStable<'tcx> {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher);
+}
+macro_rules! default_hash_impl {
+    ($($t:ty,)+) => {
+        $(impl<'tcx> AbiHashStable<'tcx> for $t {
+            #[inline]
+            fn abi_hash(&self, _tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+                ::std::hash::Hash::hash(self, hasher);
+            }
+        })*
+    };
+}
+
+default_hash_impl! { i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, }
+
+impl<'tcx> AbiHashStable<'tcx> for bool {
+    #[inline]
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        (if *self { 1u8 } else { 0u8 }).abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for str {
+    #[inline]
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        self.as_bytes().abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for String {
+    #[inline]
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        self[..].abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for Symbol {
+    #[inline]
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        self.as_str().abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx, T: AbiHashStable<'tcx>> AbiHashStable<'tcx> for [T] {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        self.len().abi_hash(tcx, hasher);
+        for item in self {
+            item.abi_hash(tcx, hasher);
+        }
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for Ty<'tcx> {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        match self.kind() {
+            ty::Bool => sym::bool.abi_hash(tcx, hasher),
+            ty::Char => sym::char.abi_hash(tcx, hasher),
+            ty::Int(int_ty) => int_ty.name_str().abi_hash(tcx, hasher),
+            ty::Uint(uint_ty) => uint_ty.name_str().abi_hash(tcx, hasher),
+            ty::Float(float_ty) => float_ty.name_str().abi_hash(tcx, hasher),
+
+            ty::Adt(adt_def, args) => {
+                adt_def.is_struct().abi_hash(tcx, hasher);
+                adt_def.is_enum().abi_hash(tcx, hasher);
+                adt_def.is_union().abi_hash(tcx, hasher);
+
+                if let Some(align) = adt_def.repr().align {
+                    align.bits().abi_hash(tcx, hasher);
+                }
+
+                if let Some(integer) = adt_def.repr().int {
+                    match integer {
+                        IntegerType::Pointer(sign) => sign.abi_hash(tcx, hasher),
+                        IntegerType::Fixed(integer, sign) => {
+                            integer.int_ty_str().abi_hash(tcx, hasher);
+                            sign.abi_hash(tcx, hasher);
+                        }
+                    }
+                }
+
+                if let Some(pack) = adt_def.repr().pack {
+                    pack.bits().abi_hash(tcx, hasher);
+                }
+
+                adt_def.repr().c().abi_hash(tcx, hasher);
+
+                for variant in adt_def.variants() {
+                    variant.name.abi_hash(tcx, hasher);
+                    for field in &variant.fields {
+                        field.name.abi_hash(tcx, hasher);
+                        let field_ty = tcx.type_of(field.did).instantiate_identity();
+                        field_ty.abi_hash(tcx, hasher);
+                    }
+                }
+                args.abi_hash(tcx, hasher);
+            }
+
+            ty::Tuple(args) if args.len() == 0 => {}
+
+            // FIXME: Not yet supported.
+            ty::Foreign(_)
+            | ty::Ref(_, _, _)
+            | ty::Str
+            | ty::Array(_, _)
+            | ty::Pat(_, _)
+            | ty::Slice(_)
+            | ty::RawPtr(_, _)
+            | ty::FnDef(_, _)
+            | ty::FnPtr(_, _)
+            | ty::Dynamic(_, _, _)
+            | ty::Closure(_, _)
+            | ty::CoroutineClosure(_, _)
+            | ty::Coroutine(_, _)
+            | ty::CoroutineWitness(_, _)
+            | ty::Never
+            | ty::Tuple(_)
+            | ty::Alias(_, _)
+            | ty::Param(_)
+            | ty::Bound(_, _)
+            | ty::Placeholder(_)
+            | ty::Infer(_)
+            | ty::UnsafeBinder(_) => unreachable!(),
+
+            ty::Error(_) => {}
+        }
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for ty::FnSig<'tcx> {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        for ty in self.inputs_and_output {
+            ty.abi_hash(tcx, hasher);
+        }
+        self.safety.is_safe().abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for ty::GenericArg<'tcx> {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        self.unpack().abi_hash(tcx, hasher);
+    }
+}
+
+impl<'tcx> AbiHashStable<'tcx> for ty::GenericArgKind<'tcx> {
+    fn abi_hash(&self, tcx: TyCtxt<'tcx>, hasher: &mut StableHasher) {
+        match self {
+            ty::GenericArgKind::Type(t) => t.abi_hash(tcx, hasher),
+            ty::GenericArgKind::Lifetime(_) | ty::GenericArgKind::Const(_) => unimplemented!(),
+        }
+    }
+}
+
+pub(crate) fn compute_hash_of_export_fn<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    instance: Instance<'tcx>,
+) -> String {
+    let def_id = instance.def_id();
+    debug_assert_matches!(tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn);
+
+    let args = instance.args;
+    let sig_ty = tcx.fn_sig(def_id).instantiate(tcx, args);
+    let sig_ty = tcx.instantiate_bound_regions_with_erased(sig_ty);
+
+    let hash = {
+        let mut hasher = StableHasher::new();
+        sig_ty.abi_hash(tcx, &mut hasher);
+        hasher.finish::<Hash128>()
+    };
+
+    hash.as_u128().to_string()
+}
diff --git a/compiler/rustc_symbol_mangling/src/lib.rs b/compiler/rustc_symbol_mangling/src/lib.rs
index ca8918e06aa..a51d7da878a 100644
--- a/compiler/rustc_symbol_mangling/src/lib.rs
+++ b/compiler/rustc_symbol_mangling/src/lib.rs
@@ -92,6 +92,7 @@
 #![cfg_attr(bootstrap, feature(let_chains))]
 #![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")]
 #![doc(rust_logo)]
+#![feature(assert_matches)]
 #![feature(rustdoc_internals)]
 // tidy-alphabetical-end
 
@@ -104,6 +105,7 @@ use rustc_middle::ty::{self, Instance, TyCtxt};
 use rustc_session::config::SymbolManglingVersion;
 use tracing::debug;
 
+mod export;
 mod hashed;
 mod legacy;
 mod v0;
@@ -296,12 +298,21 @@ fn compute_symbol_name<'tcx>(
         tcx.symbol_mangling_version(mangling_version_crate)
     };
 
-    let symbol = match mangling_version {
-        SymbolManglingVersion::Legacy => legacy::mangle(tcx, instance, instantiating_crate),
-        SymbolManglingVersion::V0 => v0::mangle(tcx, instance, instantiating_crate),
-        SymbolManglingVersion::Hashed => hashed::mangle(tcx, instance, instantiating_crate, || {
-            v0::mangle(tcx, instance, instantiating_crate)
-        }),
+    let symbol = match tcx.is_exportable(def_id) {
+        true => format!(
+            "{}.{}",
+            v0::mangle(tcx, instance, instantiating_crate, true),
+            export::compute_hash_of_export_fn(tcx, instance)
+        ),
+        false => match mangling_version {
+            SymbolManglingVersion::Legacy => legacy::mangle(tcx, instance, instantiating_crate),
+            SymbolManglingVersion::V0 => v0::mangle(tcx, instance, instantiating_crate, false),
+            SymbolManglingVersion::Hashed => {
+                hashed::mangle(tcx, instance, instantiating_crate, || {
+                    v0::mangle(tcx, instance, instantiating_crate, false)
+                })
+            }
+        },
     };
 
     debug_assert!(
diff --git a/compiler/rustc_symbol_mangling/src/v0.rs b/compiler/rustc_symbol_mangling/src/v0.rs
index f8f2714ee42..ad391d56992 100644
--- a/compiler/rustc_symbol_mangling/src/v0.rs
+++ b/compiler/rustc_symbol_mangling/src/v0.rs
@@ -26,6 +26,7 @@ pub(super) fn mangle<'tcx>(
     tcx: TyCtxt<'tcx>,
     instance: Instance<'tcx>,
     instantiating_crate: Option<CrateNum>,
+    is_exportable: bool,
 ) -> String {
     let def_id = instance.def_id();
     // FIXME(eddyb) this should ideally not be needed.
@@ -35,6 +36,7 @@ pub(super) fn mangle<'tcx>(
     let mut cx: SymbolMangler<'_> = SymbolMangler {
         tcx,
         start_offset: prefix.len(),
+        is_exportable,
         paths: FxHashMap::default(),
         types: FxHashMap::default(),
         consts: FxHashMap::default(),
@@ -93,6 +95,7 @@ pub fn mangle_internal_symbol<'tcx>(tcx: TyCtxt<'tcx>, item_name: &str) -> Strin
     let mut cx: SymbolMangler<'_> = SymbolMangler {
         tcx,
         start_offset: prefix.len(),
+        is_exportable: false,
         paths: FxHashMap::default(),
         types: FxHashMap::default(),
         consts: FxHashMap::default(),
@@ -135,6 +138,7 @@ pub(super) fn mangle_typeid_for_trait_ref<'tcx>(
     let mut cx = SymbolMangler {
         tcx,
         start_offset: 0,
+        is_exportable: false,
         paths: FxHashMap::default(),
         types: FxHashMap::default(),
         consts: FxHashMap::default(),
@@ -163,6 +167,7 @@ struct SymbolMangler<'tcx> {
     tcx: TyCtxt<'tcx>,
     binders: Vec<BinderLevel>,
     out: String,
+    is_exportable: bool,
 
     /// The length of the prefix in `out` (e.g. 2 for `_R`).
     start_offset: usize,
@@ -376,7 +381,14 @@ impl<'tcx> Printer<'tcx> for SymbolMangler<'tcx> {
                 args,
             )?;
         } else {
-            self.push_disambiguator(key.disambiguated_data.disambiguator as u64);
+            let exported_impl_order = self.tcx.stable_order_of_exportable_impls(impl_def_id.krate);
+            let disambiguator = match self.is_exportable {
+                true => exported_impl_order[&impl_def_id] as u64,
+                false => {
+                    exported_impl_order.len() as u64 + key.disambiguated_data.disambiguator as u64
+                }
+            };
+            self.push_disambiguator(disambiguator);
             self.print_def_path(parent_def_id, &[])?;
         }
 
@@ -818,8 +830,10 @@ impl<'tcx> Printer<'tcx> for SymbolMangler<'tcx> {
 
     fn path_crate(&mut self, cnum: CrateNum) -> Result<(), PrintError> {
         self.push("C");
-        let stable_crate_id = self.tcx.def_path_hash(cnum.as_def_id()).stable_crate_id();
-        self.push_disambiguator(stable_crate_id.as_u64());
+        if !self.is_exportable {
+            let stable_crate_id = self.tcx.def_path_hash(cnum.as_def_id()).stable_crate_id();
+            self.push_disambiguator(stable_crate_id.as_u64());
+        }
         let name = self.tcx.crate_name(cnum);
         self.push_ident(name.as_str());
         Ok(())
diff --git a/rustfmt.toml b/rustfmt.toml
index 7c384b876bf..d9857a7e3e7 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -19,6 +19,7 @@ ignore = [
     "/tests/debuginfo/",              # These tests are somewhat sensitive to source code layout.
     "/tests/incremental/",            # These tests are somewhat sensitive to source code layout.
     "/tests/pretty/",                 # These tests are very sensitive to source code layout.
+    "/tests/run-make/export",         # These tests contain syntax errors.
     "/tests/run-make/translation/test.rs", # This test contains syntax errors.
     "/tests/rustdoc/",                # Some have syntax errors, some are whitespace-sensitive.
     "/tests/rustdoc-gui/",            # Some tests are sensitive to source code layout.
diff --git a/tests/run-make/export/compile-interface-error/app.rs b/tests/run-make/export/compile-interface-error/app.rs
new file mode 100644
index 00000000000..f619745a711
--- /dev/null
+++ b/tests/run-make/export/compile-interface-error/app.rs
@@ -0,0 +1,3 @@
+extern crate libr;
+
+fn main() {}
diff --git a/tests/run-make/export/compile-interface-error/liblibr.rs b/tests/run-make/export/compile-interface-error/liblibr.rs
new file mode 100644
index 00000000000..906d8d7be12
--- /dev/null
+++ b/tests/run-make/export/compile-interface-error/liblibr.rs
@@ -0,0 +1,5 @@
+#![feature(export_stable)]
+
+// interface file is broken(priv fn):
+#[export_stable]
+extern "C" fn foo();
diff --git a/tests/run-make/export/compile-interface-error/rmake.rs b/tests/run-make/export/compile-interface-error/rmake.rs
new file mode 100644
index 00000000000..89474e9d4fb
--- /dev/null
+++ b/tests/run-make/export/compile-interface-error/rmake.rs
@@ -0,0 +1,9 @@
+use run_make_support::rustc;
+
+fn main() {
+    // Do not produce the interface, use the broken one.
+    rustc()
+        .input("app.rs")
+        .run_fail()
+        .assert_stderr_contains("couldn't compile interface");
+}
diff --git a/tests/run-make/export/disambiguator/app.rs b/tests/run-make/export/disambiguator/app.rs
new file mode 100644
index 00000000000..27e0e2280e5
--- /dev/null
+++ b/tests/run-make/export/disambiguator/app.rs
@@ -0,0 +1,7 @@
+extern crate libr;
+
+use libr::*;
+
+fn main() {
+    assert_eq!(S::<S2>::foo(), 2);
+}
diff --git a/tests/run-make/export/disambiguator/libr.rs b/tests/run-make/export/disambiguator/libr.rs
new file mode 100644
index 00000000000..b294d5c9e8e
--- /dev/null
+++ b/tests/run-make/export/disambiguator/libr.rs
@@ -0,0 +1,27 @@
+// `S::<S2>::foo` and `S::<S1>::foo` have same `DefPath` modulo disambiguator.
+// `libr.rs` interface may not contain `S::<S1>::foo` as private items aren't
+// exportable. We should make sure that original `S::<S2>::foo` and the one
+// produced during interface generation have same mangled names.
+
+#![feature(export_stable)]
+#![crate_type = "sdylib"]
+
+#[export_stable]
+#[repr(C)]
+pub struct S<T>(pub T);
+
+struct S1;
+pub struct S2;
+
+impl S<S1> {
+    extern "C" fn foo() -> i32 {
+        1
+    }
+}
+
+#[export_stable]
+impl S<S2> {
+    pub extern "C" fn foo() -> i32 {
+        2
+    }
+}
diff --git a/tests/run-make/export/disambiguator/rmake.rs b/tests/run-make/export/disambiguator/rmake.rs
new file mode 100644
index 00000000000..743db1933fb
--- /dev/null
+++ b/tests/run-make/export/disambiguator/rmake.rs
@@ -0,0 +1,12 @@
+use run_make_support::rustc;
+
+fn main() {
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "1")
+        .input("libr.rs")
+        .run();
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "2")
+        .input("app.rs")
+        .run();
+}
diff --git a/tests/run-make/export/extern-opt/app.rs b/tests/run-make/export/extern-opt/app.rs
new file mode 100644
index 00000000000..765c9925d5f
--- /dev/null
+++ b/tests/run-make/export/extern-opt/app.rs
@@ -0,0 +1,6 @@
+extern crate libr;
+use libr::*;
+
+fn main() {
+    assert_eq!(foo(1), 1);
+}
diff --git a/tests/run-make/export/extern-opt/libinterface.rs b/tests/run-make/export/extern-opt/libinterface.rs
new file mode 100644
index 00000000000..313cfbe7d59
--- /dev/null
+++ b/tests/run-make/export/extern-opt/libinterface.rs
@@ -0,0 +1,4 @@
+#![feature(export_stable)]
+
+#[export_stable]
+pub extern "C" fn foo(x: i32) -> i32;
diff --git a/tests/run-make/export/extern-opt/libr.rs b/tests/run-make/export/extern-opt/libr.rs
new file mode 100644
index 00000000000..026ebb4233d
--- /dev/null
+++ b/tests/run-make/export/extern-opt/libr.rs
@@ -0,0 +1,5 @@
+#![feature(export_stable)]
+#![crate_type = "sdylib"]
+
+#[export_stable]
+pub extern "C" fn foo(x: i32) -> i32 { x }
diff --git a/tests/run-make/export/extern-opt/rmake.rs b/tests/run-make/export/extern-opt/rmake.rs
new file mode 100644
index 00000000000..821e2eb2149
--- /dev/null
+++ b/tests/run-make/export/extern-opt/rmake.rs
@@ -0,0 +1,23 @@
+use run_make_support::{rustc, dynamic_lib_name};
+
+fn main() {
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "1")
+        .input("libr.rs")
+        .run();
+
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "2")
+        .input("app.rs")
+        .extern_("libr", "libinterface.rs")
+        .extern_("libr", dynamic_lib_name("libr"))
+        .run();
+
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "2")
+        .input("app.rs")
+        .extern_("libr", "interface.rs") // wrong interface format
+        .extern_("libr", dynamic_lib_name("libr"))
+        .run_fail()
+        .assert_stderr_contains("extern location for libr does not exist");
+}
diff --git a/tests/run-make/export/simple/app.rs b/tests/run-make/export/simple/app.rs
new file mode 100644
index 00000000000..ba34bdd7b56
--- /dev/null
+++ b/tests/run-make/export/simple/app.rs
@@ -0,0 +1,8 @@
+extern crate libr;
+use libr::*;
+
+fn main() {
+    let s = m::S { x: 42 };
+    assert_eq!(m::foo1(s), 42);
+    assert_eq!(m::S::foo2(1), 1);
+}
diff --git a/tests/run-make/export/simple/libr.rs b/tests/run-make/export/simple/libr.rs
new file mode 100644
index 00000000000..e10b76a6e52
--- /dev/null
+++ b/tests/run-make/export/simple/libr.rs
@@ -0,0 +1,22 @@
+#![feature(export_stable)]
+#![crate_type = "sdylib"]
+
+#[export_stable]
+pub mod m {
+    #[repr(C)]
+    pub struct S {
+        pub x: i32,
+    }
+
+    pub extern "C" fn foo1(x: S) -> i32 {
+        x.x
+    }
+
+    pub type Integer = i32;
+
+    impl S {
+        pub extern "C" fn foo2(x: Integer) -> Integer {
+            x
+        }
+    }
+}
diff --git a/tests/run-make/export/simple/rmake.rs b/tests/run-make/export/simple/rmake.rs
new file mode 100644
index 00000000000..743db1933fb
--- /dev/null
+++ b/tests/run-make/export/simple/rmake.rs
@@ -0,0 +1,12 @@
+use run_make_support::rustc;
+
+fn main() {
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "1")
+        .input("libr.rs")
+        .run();
+    rustc()
+        .env("RUSTC_FORCE_RUSTC_VERSION", "2")
+        .input("app.rs")
+        .run();
+}
diff --git a/tests/ui/attributes/export/crate-type-2.rs b/tests/ui/attributes/export/crate-type-2.rs
new file mode 100644
index 00000000000..f0379f6d797
--- /dev/null
+++ b/tests/ui/attributes/export/crate-type-2.rs
@@ -0,0 +1,2 @@
+//@ compile-flags: --crate-type=sdylib
+//~^ ERROR  `sdylib` crate type is unstable
diff --git a/tests/ui/attributes/export/crate-type-2.stderr b/tests/ui/attributes/export/crate-type-2.stderr
new file mode 100644
index 00000000000..7ce6a500113
--- /dev/null
+++ b/tests/ui/attributes/export/crate-type-2.stderr
@@ -0,0 +1,9 @@
+error[E0658]: `sdylib` crate type is unstable
+   |
+   = note: see issue #139939 <https://github.com/rust-lang/rust/issues/139939> for more information
+   = help: add `#![feature(export_stable)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/ui/attributes/export/crate-type.rs b/tests/ui/attributes/export/crate-type.rs
new file mode 100644
index 00000000000..bd092bbb1a1
--- /dev/null
+++ b/tests/ui/attributes/export/crate-type.rs
@@ -0,0 +1,2 @@
+#![crate_type = "sdylib"]
+//~^ ERROR  `sdylib` crate type is unstable
diff --git a/tests/ui/attributes/export/crate-type.stderr b/tests/ui/attributes/export/crate-type.stderr
new file mode 100644
index 00000000000..7ce6a500113
--- /dev/null
+++ b/tests/ui/attributes/export/crate-type.stderr
@@ -0,0 +1,9 @@
+error[E0658]: `sdylib` crate type is unstable
+   |
+   = note: see issue #139939 <https://github.com/rust-lang/rust/issues/139939> for more information
+   = help: add `#![feature(export_stable)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0658`.
diff --git a/tests/ui/attributes/export/exportable.rs b/tests/ui/attributes/export/exportable.rs
new file mode 100644
index 00000000000..f592fce88cd
--- /dev/null
+++ b/tests/ui/attributes/export/exportable.rs
@@ -0,0 +1,139 @@
+//@ compile-flags: -Zunstable-options -Csymbol-mangling-version=v0
+
+#![crate_type = "sdylib"]
+#![allow(incomplete_features, improper_ctypes_definitions)]
+#![feature(export_stable)]
+#![feature(inherent_associated_types)]
+
+mod m {
+    #[export_stable]
+    pub struct S;
+    //~^ ERROR private items are not exportable
+
+    pub fn foo() -> i32 { 0 }
+    //~^ ERROR only functions with "C" ABI are exportable
+}
+
+#[export_stable]
+pub use m::foo;
+
+#[export_stable]
+pub mod m1 {
+    #[repr(C)]
+    pub struct S1; // OK, public type with stable repr
+
+    struct S2;
+
+    pub struct S3;
+    //~^ ERROR types with unstable layout are not exportable
+}
+
+pub mod fn_sig {
+    #[export_stable]
+    pub fn foo1() {}
+    //~^ ERROR only functions with "C" ABI are exportable
+
+    #[export_stable]
+    #[repr(C)]
+    pub struct S;
+
+    #[export_stable]
+    pub extern "C" fn foo2(x: S) -> i32 { 0 }
+
+    #[export_stable]
+    pub extern "C" fn foo3(x: Box<S>) -> i32 { 0 }
+    //~^ ERROR function with `#[export_stable]` attribute uses type `Box<fn_sig::S>`, which is not exportable
+}
+
+pub mod impl_item {
+    pub struct S;
+
+    impl S {
+        #[export_stable]
+        pub extern "C" fn foo1(&self) -> i32 { 0 }
+        //~^ ERROR method with `#[export_stable]` attribute uses type `&impl_item::S`, which is not exportable
+
+        #[export_stable]
+        pub extern "C" fn foo2(self) -> i32 { 0 }
+        //~^ ERROR method with `#[export_stable]` attribute uses type `impl_item::S`, which is not exportable
+    }
+
+    pub struct S2<T>(T);
+
+    impl<T> S2<T> {
+        #[export_stable]
+        pub extern "C" fn foo1(&self) {}
+        //~^ ERROR generic functions are not exportable
+    }
+}
+
+pub mod tys {
+    pub trait Trait {
+        type Type;
+    }
+    pub struct S;
+
+    impl Trait for S {
+        type Type = (u32,);
+    }
+
+    #[export_stable]
+    pub extern "C" fn foo1(x: <S as Trait>::Type) -> u32 { x.0 }
+    //~^ ERROR function with `#[export_stable]` attribute uses type `(u32,)`, which is not exportable
+
+    #[export_stable]
+    pub type Type = [i32; 4];
+
+    #[export_stable]
+    pub extern "C" fn foo2(_x: Type) {}
+    //~^ ERROR function with `#[export_stable]` attribute uses type `[i32; 4]`, which is not exportable
+
+    impl S {
+        #[export_stable]
+        pub type Type = extern "C" fn();
+    }
+
+    #[export_stable]
+    pub extern "C" fn foo3(_x: S::Type) {}
+    //~^ ERROR function with `#[export_stable]` attribute uses type `extern "C" fn()`, which is not exportable
+
+    #[export_stable]
+    pub extern "C" fn foo4() -> impl Copy {
+    //~^ ERROR function with `#[export_stable]` attribute uses type `impl Copy`, which is not exportable
+        0
+    }
+}
+
+pub mod privacy {
+    #[export_stable]
+    #[repr(C)]
+    pub struct S1 {
+        pub x: i32
+    }
+
+    #[export_stable]
+    #[repr(C)]
+    pub struct S2 {
+    //~^ ERROR ADT types with private fields are not exportable
+        x: i32
+    }
+
+    #[export_stable]
+    #[repr(i32)]
+    enum E {
+    //~^ ERROR private items are not exportable
+        Variant1 { x: i32 }
+    }
+}
+
+pub mod use_site {
+    #[export_stable]
+    pub trait Trait {}
+    //~^ ERROR trait's are not exportable
+
+    #[export_stable]
+    pub const C: i32 = 0;
+    //~^ ERROR constant's are not exportable
+}
+
+fn main() {}
diff --git a/tests/ui/attributes/export/exportable.stderr b/tests/ui/attributes/export/exportable.stderr
new file mode 100644
index 00000000000..0f6469d35c3
--- /dev/null
+++ b/tests/ui/attributes/export/exportable.stderr
@@ -0,0 +1,130 @@
+error: private items are not exportable
+  --> $DIR/exportable.rs:10:5
+   |
+LL |     pub struct S;
+   |     ^^^^^^^^^^^^
+   |
+note: is only usable at visibility `pub(crate)`
+  --> $DIR/exportable.rs:10:5
+   |
+LL |     pub struct S;
+   |     ^^^^^^^^^^^^
+
+error: private items are not exportable
+  --> $DIR/exportable.rs:123:5
+   |
+LL |     enum E {
+   |     ^^^^^^
+   |
+note: is only usable at visibility `pub(self)`
+  --> $DIR/exportable.rs:123:5
+   |
+LL |     enum E {
+   |     ^^^^^^
+
+error: trait's are not exportable
+  --> $DIR/exportable.rs:131:5
+   |
+LL |     pub trait Trait {}
+   |     ^^^^^^^^^^^^^^^
+
+error: constant's are not exportable
+  --> $DIR/exportable.rs:135:5
+   |
+LL |     pub const C: i32 = 0;
+   |     ^^^^^^^^^^^^^^^^
+
+error: only functions with "C" ABI are exportable
+  --> $DIR/exportable.rs:13:5
+   |
+LL |     pub fn foo() -> i32 { 0 }
+   |     ^^^^^^^^^^^^^^^^^^^
+
+error: types with unstable layout are not exportable
+  --> $DIR/exportable.rs:27:5
+   |
+LL |     pub struct S3;
+   |     ^^^^^^^^^^^^^
+
+error: only functions with "C" ABI are exportable
+  --> $DIR/exportable.rs:33:5
+   |
+LL |     pub fn foo1() {}
+   |     ^^^^^^^^^^^^^
+
+error: function with `#[export_stable]` attribute uses type `Box<fn_sig::S>`, which is not exportable
+  --> $DIR/exportable.rs:44:5
+   |
+LL |     pub extern "C" fn foo3(x: Box<S>) -> i32 { 0 }
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^------^^^^^^^^
+   |                               |
+   |                               not exportable
+
+error: method with `#[export_stable]` attribute uses type `&impl_item::S`, which is not exportable
+  --> $DIR/exportable.rs:53:9
+   |
+LL |         pub extern "C" fn foo1(&self) -> i32 { 0 }
+   |         ^^^^^^^^^^^^^^^^^^^^^^^-----^^^^^^^^
+   |                                |
+   |                                not exportable
+
+error: method with `#[export_stable]` attribute uses type `impl_item::S`, which is not exportable
+  --> $DIR/exportable.rs:57:9
+   |
+LL |         pub extern "C" fn foo2(self) -> i32 { 0 }
+   |         ^^^^^^^^^^^^^^^^^^^^^^^----^^^^^^^^
+   |                                |
+   |                                not exportable
+
+error: generic functions are not exportable
+  --> $DIR/exportable.rs:65:9
+   |
+LL |         pub extern "C" fn foo1(&self) {}
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error: function with `#[export_stable]` attribute uses type `(u32,)`, which is not exportable
+  --> $DIR/exportable.rs:81:5
+   |
+LL |     pub extern "C" fn foo1(x: <S as Trait>::Type) -> u32 { x.0 }
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^------------------^^^^^^^^
+   |                               |
+   |                               not exportable
+
+error: function with `#[export_stable]` attribute uses type `[i32; 4]`, which is not exportable
+  --> $DIR/exportable.rs:88:5
+   |
+LL |     pub extern "C" fn foo2(_x: Type) {}
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^----^
+   |                                |
+   |                                not exportable
+
+error: function with `#[export_stable]` attribute uses type `extern "C" fn()`, which is not exportable
+  --> $DIR/exportable.rs:97:5
+   |
+LL |     pub extern "C" fn foo3(_x: S::Type) {}
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^-------^
+   |                                |
+   |                                not exportable
+
+error: function with `#[export_stable]` attribute uses type `impl Copy`, which is not exportable
+  --> $DIR/exportable.rs:101:5
+   |
+LL |     pub extern "C" fn foo4() -> impl Copy {
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------
+   |                                 |
+   |                                 not exportable
+
+error: ADT types with private fields are not exportable
+  --> $DIR/exportable.rs:116:5
+   |
+LL |     pub struct S2 {
+   |     ^^^^^^^^^^^^^
+   |
+note: `x` is private
+  --> $DIR/exportable.rs:118:9
+   |
+LL |         x: i32
+   |         ^^^^^^
+
+error: aborting due to 16 previous errors
+
diff --git a/tests/ui/attributes/export/lang-item.rs b/tests/ui/attributes/export/lang-item.rs
new file mode 100644
index 00000000000..b923b41a957
--- /dev/null
+++ b/tests/ui/attributes/export/lang-item.rs
@@ -0,0 +1,8 @@
+#![feature(no_core, lang_items, export_stable)]
+#![allow(incomplete_features)]
+#![crate_type = "sdylib"]
+#![no_core]
+
+#[lang = "sized"]
+//~^ ERROR lang items are not allowed in stable dylibs
+trait Sized {}
diff --git a/tests/ui/attributes/export/lang-item.stderr b/tests/ui/attributes/export/lang-item.stderr
new file mode 100644
index 00000000000..8c0741bdb6f
--- /dev/null
+++ b/tests/ui/attributes/export/lang-item.stderr
@@ -0,0 +1,8 @@
+error: lang items are not allowed in stable dylibs
+  --> $DIR/lang-item.rs:6:1
+   |
+LL | #[lang = "sized"]
+   | ^^^^^^^^^^^^^^^^^
+
+error: aborting due to 1 previous error
+
diff --git a/tests/ui/feature-gates/feature-gate-export_stable.rs b/tests/ui/feature-gates/feature-gate-export_stable.rs
new file mode 100644
index 00000000000..5d05fee059b
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-export_stable.rs
@@ -0,0 +1,5 @@
+#![crate_type="lib"]
+
+#[export_stable]
+//~^ ERROR the `#[export_stable]` attribute is an experimental feature
+pub mod a {}
diff --git a/tests/ui/feature-gates/feature-gate-export_stable.stderr b/tests/ui/feature-gates/feature-gate-export_stable.stderr
new file mode 100644
index 00000000000..6beb52a77e5
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-export_stable.stderr
@@ -0,0 +1,13 @@
+error[E0658]: the `#[export_stable]` attribute is an experimental feature
+  --> $DIR/feature-gate-export_stable.rs:3:1
+   |
+LL | #[export_stable]
+   | ^^^^^^^^^^^^^^^^
+   |
+   = note: see issue #139939 <https://github.com/rust-lang/rust/issues/139939> for more information
+   = help: add `#![feature(export_stable)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0658`.