Skip to content

Initial support for dynamically linked crates #134767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Bryanskiy
Copy link
Contributor

@Bryanskiy Bryanskiy commented Dec 25, 2024

This PR is an initial implementation of rust-lang/rfcs#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:

#[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:

#[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: rust-lang/rfcs#3435 (comment)

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(#134767 (comment)):

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:

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

#[extern]
pub extern "C" fn foo() -> i32 {
    42
}
// 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 .

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:

#[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). For now, it includes:

  • hash of the type name for primitive types
  • for ADT types with public fields the implementation follows this 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

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: Initial support for dynamically linked crates #134767 (comment)
  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)
  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.

@rustbot
Copy link
Collaborator

rustbot commented Dec 25, 2024

r? @oli-obk

rustbot has assigned @oli-obk.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added A-run-make Area: port run-make Makefiles to rmake.rs S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue. labels Dec 25, 2024
@Bryanskiy
Copy link
Contributor Author

cc @m-ou-se

@rust-log-analyzer

This comment has been minimized.

@bjorn3
Copy link
Member

bjorn3 commented Dec 26, 2024

However, in that case, we encounter a "disambiguation" issue:

I think that is indicative of a fundamental issue with your implementation. Adding new private impls should not change the ABI. The whole point of the RFC is to allow changing the implementation of the dylib without changing it's ABI. We already have support for dylibs where the ABI depends on the implementation.

@bjorn3
Copy link
Member

bjorn3 commented Dec 26, 2024

Also the interface file is missing either the StableCrateId or it's constituent parts (crate name and all -Cmetadata arguments). Without this it is impossible for the consumer to mangle symbols in the same way as the dylib itself.

Replacing all function bodies in an interface file with loop {} does not work in the presence of RPIT. And serializing it as a rust source file misses the expansion context which is essential for hygiene. I did much rather use a format like JSON of while we don't guarantee a stable ABI across rustc versions a binary format.

@bjorn3
Copy link
Member

bjorn3 commented Dec 26, 2024

component 4: stable ABI mangling scheme

A stable ABI across rustc versions is a lot more involved than just mangling all symbols the same. You have to make sure multiple copies of the standard library seamlessly interoperate including using the same TypeId for &str and String (and thus identical layout for these) for panics to work, you have to make sure the same #[global_allocator] is used, all standard library types remain layout compatible across versions (which would be very limiting and eg lock is in forever on which OSes use pthread_mutex and which use futex) and more. Realistically I don't think a stable ABI across rustc versions is possible without severely limiting what can be passed across the ABI boundary (crABI), which is completely orthogonal to making rust dylibs work across crate versions. crABI can work with cdylib's just as easily.

Edit: To put it another way, I did strongly prefer if stable ABI across rustc versions and stable ABI across crate versions with a single rustc version are treated as entirely separate problems implemented entirely separately. For stable ABI across crate versions you don't need to generate interface files. You can just reuse the existing rmeta file format, but only encode the subset of the items corresponding to the stable ABI.

@oli-obk oli-obk removed their assignment Jan 7, 2025
@Bryanskiy
Copy link
Contributor Author

@bjorn3

I think that is indicative of a fundamental issue with your implementation. Adding new private impls should not change the ABI. The whole point of the RFC is to allow changing the implementation of the dylib without changing it's ABI.

I can suggest the following solution for the above problem with impl's:

Split indices (disambiguators) into 2 sets: $S_1 = { 0, 1, ..., k }$, $S_2 = { k + 1, ..., n }$ where $k$ - number of exported impls, $n$ - total number of impls. For the exportable impls we assign indices from $S_1$ based on the their order in the source code. For the other impls we assign indices from $S_2$ in any way. This approach is stable across rustc versions and doesn't depend on private items.

Also the interface file is missing either the StableCrateId or it's constituent parts (crate name and all -Cmetadata arguments).

  1. Crate name is encoded in the interface name: lib{crate_name}.rs.
  2. -Cmetadata arguments are not yet supported. At the moment, dependence on them has been removed for stable "mangled" symbols.

Replacing all function bodies in an interface file with loop {} does not work in the presence of RPIT.

  1. I used loop {} as a temporary solution since fn foo(); syntax is not allowed 😄.
  2. Regarding the RPIT, yes, it is incompatible with the "header file" concept. But anyway, as you have already said, stable ABI should not depend on implementation. However, computing a hidden type is depends on implementation. So, I don't believe it is possible to support RPIT's without imposing strict limitations.

And serializing it as a rust source file misses the expansion context which is essential for hygiene. I did much rather use a format like JSON of while we don't guarantee a stable ABI across rustc versions a binary format.

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 source code. Stable metadata format is not a part of the RFC.

component 4: stable ABI mangling scheme
A stable ABI across rustc versions is a lot more involved than just mangling all symbols the same. You have to make sure multiple copies of the standard library seamlessly interoperate including using the same TypeId for &str and String ...

"stable ABI" is a bad wording here. Neither I nor the RFC offers a stable ABI. And all these issues are outside the scope of the proposal.

@bjorn3
Copy link
Member

bjorn3 commented Jan 13, 2025

One of the main objectives of this proposal is to allow building the library and the application with different compiler versions.

"stable ABI" is a bad wording here. Neither I nor the RFC offers a stable ABI. And all these issues are outside the scope of the proposal.

These two statements are conflicting with each other. Being able to build a library and application with a different compiler version requires both to share an ABI, in other words it requires having a stable ABI.

@Bryanskiy
Copy link
Contributor Author

Bryanskiy commented Jan 13, 2025

These two statements are conflicting with each other. Being able to build a library and application with a different compiler version requires both to share an ABI, in other words it requires having a stable ABI.

Only extern "C" functions and types with stable representation are allowed to be "exportable" right now. https://github.com/m-ou-se/rfcs/blob/export/text/0000-export.md#the-export-attribute.

@bors

This comment was marked as resolved.

@rust-log-analyzer

This comment has been minimized.

@bors

This comment was marked as resolved.

@rustbot rustbot added the A-attributes Area: Attributes (`#[…]`, `#![…]`) label Feb 11, 2025
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@Bryanskiy Bryanskiy force-pushed the dylibs-3 branch 3 times, most recently from 683025c to e213390 Compare April 7, 2025 15:25
@petrochenkov
Copy link
Contributor

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 7, 2025
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 7, 2025
Initial support for dynamically linked crates

This PR is an initial implementation of rust-lang/rfcs#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 with only "exported" items included, and 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);
```

 In this example `bar` function is not a part of the interface as it doesn't have `#[export]` attribute.
 To emit interface use a new `sdylib` crate type which is basically the same as `dylib`,  but it also produces the interface as a second artifact. The current interface name is `lib{crate_name}.rs`.

 Interface generator was implemented as part of the HIR pretty-printer.  In order to filter out unnecessary items,  the `PpAnn` trait was used.
#### 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 was rejected because it is not a part of the RFC. ([discussion](rust-lang/rfcs#3435 (comment)))

Regarding the design with interfaces there are 2 possibilities: manually written or auto-generated. I would personally prefer the auto-generated interface for the following reason: we can put it in the dynamic library instead of metadata, which will make it completely invisible to users. (this was my initial plan, but since the PR is already big enough, I decided to postpone it)

But even if we end up with a different design, I believe the interface generator could be a useful tool for testing and experimenting with the entire feature.
### 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. For now, it's assumed that interface and dynamic lib are located in the same directory. `extern dyn crate` annotation serves as a signal for the building of a dynamic dependency.

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 dyn crate simple_lib;

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

```
// Generate interface, build library.
rustc +toolchain1 lib.rs -Csymbol-mangling-version=v0

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

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 is _mostly_ independent of the compiler internals.

The first exception is an impl's disambiguation. 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;

// This impl is not part of the interface.
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. However, I have not found the right way to get this order, so I decided to use:
  1. The sequential number of an impl during the `intravisit::Visitor` traversal.
  2. A new attribute `#[rustc_stable_impl_id]` that outputs this sequential number as a compiler error. If the visitor's implementation is changed, the corresponding test will fail. Then you will need to rewrite the implementation of `stable_order_of_exportable_impls` query to preserve order.

P.S. is it possible to compare spans instead?

The second exception 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.

#### hash of the signature

Second part of the symbol name is 128 bit hash containing _relevant_ type information. 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.
@bors
Copy link
Collaborator

bors commented Apr 7, 2025

⌛ Trying commit e213390 with merge 4ea9e0d...

@bors
Copy link
Collaborator

bors commented Apr 7, 2025

☀️ Try build successful - checks-actions
Build commit: 4ea9e0d (4ea9e0dd4fef386791084d17eaa5e0d01cba735b)

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (4ea9e0d): comparison URL.

Overall result: ❌✅ regressions and improvements - no action needed

Benchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf.

@bors rollup=never
@rustbot label: -S-waiting-on-perf -perf-regression

Instruction count

This is the most reliable metric that we have; it was used to determine the overall result at the top of this comment. However, even this metric can sometimes exhibit noise.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.3% [0.3%, 0.3%] 2
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.2% [-0.2%, -0.2%] 1
All ❌✅ (primary) - - 0

Max RSS (memory usage)

Results (primary 1.1%, secondary 0.2%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
4.2% [4.2%, 4.2%] 1
Regressions ❌
(secondary)
1.7% [1.3%, 2.3%] 5
Improvements ✅
(primary)
-2.0% [-2.0%, -2.0%] 1
Improvements ✅
(secondary)
-2.3% [-2.4%, -2.2%] 3
All ❌✅ (primary) 1.1% [-2.0%, 4.2%] 2

Cycles

Results (primary -1.2%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
-1.2% [-1.2%, -1.2%] 1
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) -1.2% [-1.2%, -1.2%] 1

Binary size

Results (secondary 0.0%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.0% [0.0%, 0.0%] 3
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) - - 0

Bootstrap: 778.658s -> 779.323s (0.09%)
Artifact size: 366.00 MiB -> 366.02 MiB (0.00%)

@rustbot rustbot removed the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Apr 7, 2025
@petrochenkov
Copy link
Contributor

@bjorn3 Do you want to take a final look?

If not, r=me with the FIXME #134767 (comment) added.

@petrochenkov petrochenkov removed their assignment Apr 7, 2025
@petrochenkov petrochenkov added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Apr 11, 2025
@petrochenkov
Copy link
Contributor

I'll nominate this for the language team to get a formal approval for merging.
This is a prototype implementation for RFC #3435 "#[export] (dynamically linked crates)", which is also related to one of the 2025H1 project goals Research: How to achieve safety when linking separately compiled code.

@petrochenkov petrochenkov added I-lang-nominated Nominated for discussion during a lang team meeting. S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Apr 11, 2025
@bors
Copy link
Collaborator

bors commented Apr 14, 2025

☔ The latest upstream changes (presumably #139766) made this pull request unmergeable. Please resolve the merge conflicts.

@nikomatsakis
Copy link
Contributor

Hi all,

I am happy to serve as champion for a lang-team experiment on this work. We would need a feature gate and a suitable tracking issue to be created:

https://lang-team.rust-lang.org/how_to/experiment.html

You can put me down as the champion/liaison though. And @m-ou-se you and I need to setup a regular sync time for this and other things. =) But that is better suited to Zulip...

@nikomatsakis
Copy link
Contributor

@rustbot labels -I-lang-nominated

@rustbot rustbot removed the I-lang-nominated Nominated for discussion during a lang team meeting. label Apr 16, 2025
@m-ou-se
Copy link
Member

m-ou-se commented Apr 16, 2025

I am happy to serve as champion for a lang-team experiment on this work. We would need a feature gate and a suitable tracking issue to be created

Created a tracking issue for #[export]: #139939

Comment on lines +212 to +213
/// Allows using `#[export]` which indicates that an item is exportable.
(internal, export, "CURRENT_RUSTC_VERSION", None),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can now add #139939 as the tracking issue (and move this to the section for features with a tracking issue).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-attributes Area: Attributes (`#[…]`, `#![…]`) A-meta Area: Issues & PRs about the rust-lang/rust repository itself A-run-make Area: port run-make Makefiles to rmake.rs S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue. T-rustdoc Relevant to the rustdoc team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.