Skip to content

Latest commit

 

History

History
196 lines (162 loc) · 8.65 KB

File metadata and controls

196 lines (162 loc) · 8.65 KB

Name Mangling Problems on Cargo

The Rust package manager Cargo permits multiple versions of the same crate to coexist within a single build. To prevent unintended interactions, Cargo employs crate name mangling, assigning each version a distinct identifier and isolating it within a separate namespace. From the perspective of the dependency resolver, this strategy provides a mechanism for satisfying version constraints.

From the perspective of client code, however, such strategy can change program behavior in two ways:

  • Type/Trait incompatibility: crates may export items under identical nominal paths (e.g., url::Url), yet these are treated as distinct and incompatible types, even when their structural definitions coincide.
  • Semantic incompatibility: functions with identical type signatures may exhibit divergent semantic contracts across versions, yielding subtle behavioral incompatibilities that are not detected at compile time.

This repository demonstrates how Cargo’s name-mangling strategy (using url as an example) can lead to such problems.


Project Structure

graph TD
    app["app"]
    mid_a["mid-a"]
    mid_b["mid-b"]

    subgraph urls["url crates"]
        direction LR
        url1["url v1"]
        dummy["⟵ incompatible ⟶"]
        url2["url v2"]
    end

    app --> mid_a
    app --> mid_b
    mid_a -- "depends on v1.0" --> url1
    mid_b -- "depends on v2.0" --> url2

    classDef note fill:transparent,stroke:transparent,color:#888,font-style:italic
    class dummy note
Loading
cargo-mangling/
├── mid-a/        # depends on url v1, re-exports url::Url
├── mid-b/        # depends on url v2, re-exports url::Url + extra helpers
└── app/
    └── src/
        └── bin/
            ├── ng1.rs  # compile-time error (type incompatibility)
            ├── ng2.rs  # runtime error (semantic incompatibility)
            ├── ng3.rs  # compile-time error (trait incompatibility)
            ├── ok1.rs  # works (disjoint usage)
            ├── ok2.rs  # works (string bridge, safe)
            └── ok3.rs  # works (explicit conversion to v2 Url)
  • mid_a depends on url = "1" and re-exports url::Url.
  • mid_b depends on url = "2" and re-exports url::Url, with additional helper APIs.
  • app imports both and provides multiple binaries (src/bin/*.rs) to demonstrate different scenarios.

Initial Setup

cargo build --manifest-path mid-a/Cargo.toml && cargo build --manifest-path mid-b/Cargo.toml

How Can I Check the Duplication?

$ cargo tree --duplicates
idna v0.1.5
└── url v1.7.2
    └── mid_a v0.1.0 (/home/yudaitnb/cargo-mangling-examples/mid-a)
        └── app v0.1.0 (/home/yudaitnb/cargo-mangling-examples/app)

idna v0.5.0
└── url v2.5.2
    └── mid_b v0.1.0 (/home/yudaitnb/cargo-mangling-examples/mid-b)
        └── app v0.1.0 (/home/yudaitnb/cargo-mangling-examples/app)

percent-encoding v1.0.1
└── url v1.7.2 (*)

percent-encoding v2.3.1
├── form_urlencoded v1.2.1
│   └── url v2.5.2 (*)
└── url v2.5.2 (*)

Scenarios

Scenario Mechanism Build Runtime Risk profile
NG1 Type mismatch Compile-time safe, not fixable at the application layer alone.
NG2 Semantic drift across versions Hard to find the cause
NG3 Trait-boound mismatch Compile-time safe, not fixable at the application layer alone.
cd app/
cargo build --bin ng1 
cargo build --bin ng2
cargo run --bin ng2
cargo build --bin ng3

NG1 (compile-time type error)

Passing mid_a::Url (from url v1) into a function expecting mid_b::Url (from url v2).
→ Different crate IDs → different types → compile-time failure.

~/app$ cargo build --bin ng1
error[E0308]: mismatched types
  --> src/bin/ng1.rs:3:20
   |
3  |     mid_b::consume(u);
   |     -------------- ^ expected `mid_b::Url`, found `mid_a::Url`
   |     |
   |     arguments to this function are incorrect
   |
note: two different versions of crate `url` are being used; two types coming from two different versions of the same crate are different types even if they look the same
   --> /.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-1.7.2/src/lib.rs:154:1
    |
154 | pub struct Url {
    | ^^^^^^^^^^^^^^ this is the found type `mid_a::Url`
    |
   ::: /home/yudaitnb/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/url-2.5.4/src/lib.rs:227:1
    |
227 | pub struct Url {
    | ^^^^^^^^^^^^^^ this is the expected type `mid_b::Url`
    |
   ::: src/bin/ng1.rs:4:13
    |
4   |     let u = mid_a::make();   
    |             ----- one version of crate `url` used here, as a dependency of crate `mid_a`
...
12  |     mid_b::consume(u);
    |     ----- one version of crate `url` used here, as a dependency of crate `mid_b`
    = help: you can use `cargo tree` to explore your dependency tree
note: function defined here
   --> /home/yudaitnb/cargo-mangling/mid-b/src/lib.rs:2:8
    |
2   | pub fn consume(_u: Url) {}
    |        ^^^^^^^

Observation: Compile-time safe (caught by the type system), but surprising when identical-looking type paths come from different crate versions.

NG2 (semantic mismatch across versions)

The helper port_or_default returns Option<u16>, but its semantics diverge:

  • In url v1, gopher:// is treated as having a default port 70.
  • In url v2.2.0~, gopher is not considered special, so the result is None when no port is specified.

Note: This behavioral change is not explicitly mentioned in the url crate’s CHANGELOG, but according to the commit history it appears to have been introduced between v2.2.0 and v2.1.1.

~/app$ cargo run --bin ng2
thread 'main' panicked at 'NG2: port_or_default mismatch:
  v1: Some(70)
  v2: None
  src: gopher://example.com/'

Observation: Not compile-time safe. This mismatch only surfaces at runtime as a failed assertion.

NG3 (compile-time trait identity mismatch across versions)

A value whose type is expressed via impl url::form_urlencoded::Target on the v1 side (from mid_a) cannot satisfy a generic bound T: url::form_urlencoded::Target on the v2 side (from mid_b). Even though the path looks identical in source, the trait identities differ across versions, so the trait bound is not satisfied.

~/app$ cargo run --bin ng3
error[E0277]: the trait bound `impl url::form_urlencoded::Target: form_urlencoded::Target` is not satisfied
  --> src/bin/ng3.rs:7:27
   |
 7 |     mid_b::consume_target(t);
   |     --------------------- ^ the trait `form_urlencoded::Target` is not implemented for `impl url::form_urlencoded::Target`
   |     |
   |     required by a bound introduced by this call
   |
   = help: the following other types implement trait `form_urlencoded::Target`:
             &'a mut String
             String
             url::UrlQuery<'a>
note: required by a bound in `consume_target`
  --> /home/yudaitnb/cargo-mangling-examples/mid-b/src/lib.rs:11:26
   |
11 | pub fn consume_target<T: url::form_urlencoded::Target>(mut t: T) {
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `consume_target`

Observation: Compile-time safe (caught by the type system), but surprising when identical-looking trait paths come from different crate versions.

References