Skip to content

Tracking issue for RFC 1861: Extern types #43467

Description

@aturon

This is a tracking issue for RFC 1861 "Extern types".

Steps:

Unresolved questions:

Activity

  1. added
    B-RFC-approvedBlocker: Approved by a merged RFC but not yet implemented.
    T-langRelevant to the language team
    on Jul 25, 2017
  2. changed the title [-]Tracking issue for RFC 1861: Extern tyupes[/-] [+]Tracking issue for RFC 1861: Extern types[/+] on Jul 25, 2017
  3. jethrogb commented on Jul 25, 2017

    @jethrogb
    Contributor

    This is not explicitly mentioned in the RFC, but I'm assuming different instances of extern type are actually different types? Meaning this would be illegal:

    extern {
        type A;
        type B;
    }
    
    fn convert_ref(r: &A) -> &B { r }
  4. canndrew commented on Jul 25, 2017

    @canndrew
    Contributor

    @jethrogb That's certainly the intention, yes.

  5. glaebhoerl commented on Jul 25, 2017

    @glaebhoerl
    Contributor

    Relatedly, is deciding whether we want to call it extern type or extern struct something that can still be done as part of the stabilization process, or is the extern type syntax effectively final as of having accepted the RFC?

    EDIT: rust-lang/rfcs#2071 is also relevant here w.r.t. the connotations of type "aliases". In stable Rust a type declaration is "effect-free" and just a transparent alias for some existing type. Both extern type and type Foo = impl Bar would change this by making it implicitly generate a new module-scoped existential type or type constructor (nominal type) for it to refer to.

  6. Ericson2314 commented on Jul 25, 2017

    @Ericson2314
    Contributor

    Can we get a bullet for the panic vs DynSized debate?

  7. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    on Jul 27, 2017
  8. plietar commented on Aug 10, 2017

    @plietar
    Contributor

    I've started working on this, and I have a working simple initial version (no generics, no DynSized).

    I've however noticed a slight usability issue. In FFI code, it's frequent for raw pointers to be initialized to null using std::ptr::null/null_mut. However, the function only accepts sized type arguments, since it would not be able to pick a metadata for the fat pointer.

    Despite being unsized, extern types are used through thin pointers, so it should be possible to use std::ptr::null.

    It is still possible to cast an integer to an extern type pointer, but this is not as nice as just using the function designed for this. Also this can never be done in a generic context.

    extern {
        type foo;
    }
    fn null_foo() -> *const foo {
        0usize as *const foo
    }

    Really we'd want is a new trait to distinguish types which use thin pointers. It would be implemented automatically for all sized types and extern types. Then the cast above would succeed whenever the type is bounded by this trait. Eg, the function std::ptr::null becomes :

    fn null<T: ?Sized + Thin>() -> *const T {
        0usize as *const T
    }

    However there's a risk of more and more such traits creeping up, such as DynSized, making it confusing for users. There's also some overlap with the various custom RFCs proposals which allow arbitrary metadata. For instance, instead of Thin, Referent<Meta=()> could be used

  9. SimonSapin commented on Aug 10, 2017

    @SimonSapin
    Contributor

    I think we can add extern types now and live with str::ptr::null not supporting them for a while until we figure out what to do about Thin/DynSized/Referent<Meta=…> etc.

  10. plietar commented on Aug 10, 2017

    @plietar
    Contributor

    @SimonSapin yeah, it's definitely a minor concern for now.

    I do think this problem of not having a trait bound to express "this type may be unsized be must have a thin pointer" might crop up in other places though.

  11. SimonSapin commented on Aug 11, 2017

    @SimonSapin
    Contributor

    Oh yeah, I agree we should solve that eventually too. I’m only saying we might not need to solve all of it before we ship any of it.

  12. plietar commented on Sep 3, 2017

    @plietar
    Contributor

    I've pushed an initial implementation in #44295

  13. 315 remaining items

  14. crumblingstatue commented on Oct 24, 2024

    @crumblingstatue
    Contributor

    Since no one seems to have asked this question yet:

    The current "idiom" for opaque types is having a _opaque: [u8; 0] field.
    This approach supports having PhantomData fields for supporting things like adding lifetime annotations on the type.
    I make very heavy use of this in rust-sfml:

    #[repr(C)]
    pub struct Shader<'texture> {
        _opaque: [u8; 0],
        // Keeps track of the lifetime of the texture the Shader borrows,
        // but of which Rust has no idea otherwise, since
        // the field that "borrows" the texture is on the C side
        _texture: PhantomData<&'texture Texture>,
    }

    How would I go about this using extern types?
    If extern types cannot support this, then it's not really a universal replacement for the _opaque: [u8; 0] field "workaround".

  15. programmerjake commented on Oct 24, 2024

    @programmerjake
    Member

    How would I go about this using extern types?

    I would assume by doing something like this:

    extern {
        type ShaderExtTy;
    }
    
    #[repr(transparent)]
    pub struct Shader<'texture> {
        inner: ShaderExtTy,
        _phantom: PhantomData<&'texture Texture>,
    }
  16. crumblingstatue commented on Oct 24, 2024

    @crumblingstatue
    Contributor

    @programmerjake
    Perhaps... Although it would have to be

    #[repr(transparent)]
    pub struct Shader<'texture> {
        _phantom: std::marker::PhantomData<&'texture Texture>,
        inner: ShaderExtTy,
    }

    Since only the last field can be unsized.

  17. CatsAreFluffy commented on Dec 2, 2024

    @CatsAreFluffy

    The third unresolved question shouldn't be a problem anymore, since LLVM now uses opaque pointer types.

  18. programmerjake commented on Dec 2, 2024

    @programmerjake
    Member

    The third unresolved question shouldn't be a problem anymore, since LLVM now uses opaque pointer types.

    it is probably still a problem for rustc_codegen_gcc since libgccjit has separate pointer types for each pointee type.

  19. RalfJung commented on Dec 2, 2024

    @RalfJung
    Member

    Hopefully, GCC isn't quite so sensitive about which pointee type needs to be picked. (I forgot why it was important for LLVM for void*.) But anyway given that the GCC backend is not complete yet, I don't think we should block this RFC on that -- it's "just" one more item on the list of things whose implementation is still incomplete.

  20. added a commit that references this issue on Oct 7, 2025
  21. cosmicexplorer commented on Jan 13, 2026

    @cosmicexplorer

    I was deeply unfamiliar with both this RFC as well as the sized_hierarchy one. Having just read both of those, I would like to venture a potential counterexample to the conjecture proposed by @kennytm in #43467 (comment):

    While it sounds good in theory I doubt in practice if there is any actual scenario where the FFI type specified the exact alignment yet the size and all fields are implementation details. Usually you either have none or all of these 3 details.

    I believe struct dirent used by readdir() and getdents() is just such an example where alignment is extremely significant and in fact required by POSIX for the separate entrypoint posix_getdents():

    Since the d_reclen value for the last entry in buf includes padding to satisfy alignment requirements, applications can grow the buffer and call posix_getdents() again to append to it without needing to perform an alignment calculation.

    How Alignment Can be Known without Layout

    A few important additional concerns about alignment here:

    1. The buffer provided to posix_getdents() is a memory address and length, which in most cases are provided in most cases directly to the operating system via syscall arguments.
    2. This is starkly in contrast to fread()/fwrite(), where the libc is allowed to perform its own caching on top of the OS block layer through the FILE* data structure. The libc's internal control over extra opaque allocations necessarily involves knowledge of alignment.
    3. However, posix_getdents() works under the completely opposite theory as compared to FILE*, and much closer to the atomicity guarantees available e.g. for pipes and io_uring, in allowing the user to provide a buffer directly to the OS, and which the libc doesn't perform any interpretation of. posix_getdents() also happens to invoke a much stronger atomicity guarantee:

    If posix_getdents() is called concurrently with an operation that adds, deletes, or modifies a directory entry, the results from posix_getdents() shall reflect either all of the effects of the concurrent operation or none of them.

    libc and ABI Guarantees

    I see no case that you specifically only want to know align_of::<FILE>() == align_of::<usize>() and nothing else.

    The C interface describes why:

    int posix_getdents(int fd, void *buf, size_t len, int flags)

    void* is intentionally used to avoid source-level API breakage, even in the event of the OS deciding to add or modify the interpretation of fields (and therefore introduce ABI breakage that requires a recompile). I see that @kennytm in fact predicts one of the exact issues at play here (that struct dirent always ends with a runtime-sized array):

    The only exception might be dynamic-sized array, like an array tail like xxxx_t tail[0], or C-style string const char* / const wchar_t*, but I think they are a different kind of Custom DST that shouldn't be hacked through extern type.

    In fact, an earlier revision of POSIX specifies that these are in fact always null-terminated strings (https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/dirent.h.html#tag_14_08_05) and the 2024 update with posix_dent in fact has the d_name field at the end of the struct.

    One important note that the current implementation of std::fs::read_dir() has a lengthy comment about is the remarkable variance in definitions across platforms for the immediate d_name field--usually this is a C11 conditionally-supported VLA, but it often falls back to d_name: [MaybeUninit<u8>; 0], d_name: [MaybeUninit<u8>; 1], or even d_name: [MaybeUninit<u8>; 256], where 256 == NAME_MAX. This allows specific implementations to provide the record length and alignment length statically:

    Implementations that return fixed-length entries can simply set d_reclen to that length in posix_getdents().

    Relationship to sized_hierarchy

    It also allows us to demonstrate the sizing hierarchy (see #144404) at play in terms of nested lifetimes resulting from sequential parsing operations, which aligns precisely with the discussion arising from the externref proposal for the sized_hierarchy RFC:

    1. The end user of e.g. a Rust getdents() wrapper owns the buffer used for all directory entries within a getdents() call, so they have to know the alignment of posix_dent in order to allocate the buffer correctly (although POSIX also specifies they can just fallback to an alignment of 1 like malloc() gives you).
      • It also strikes me that alignment being a necessary prerequisite to dynamically allocate memory isn't yet mentioned in the core::marker::Aligned trait RFC RFC: Aligned trait rfcs#3319.
      • This would seem to make alignment deeply related to ownership, analogous to the Value supertrait of Pointee proposed in #144404.
    2. Then, it strikes me that externref seems remarkably appropriate for modelling the direct kernel-to-user writes performed in the getdents() syscall, as it's memory the kernel has to take special care around storing and loading, and which creates its own ABI.
      • Notably, the C header files defined in the kernel for struct dirent layout are the exact struct definitions that glibc and musl propagate to their users.
    3. The fact some platforms can make d_recordlen entirely known at compile time is curious:
      • They would still need to access a custom vtable describing the layout of a custom DST ending with a C11 VLA (the rest is just padded with nulls up to the next alignment boundary),
      • However, splitting up the buffer contents into statically-known records can be done in parallel (as a slice), without the serial data dependency that an Iterator with a DSTs for each directory entry would require.
    4. In order to actually interpret the getdents() or posix_dent records, we would finally require custom DSTs using vtables with actual info about field offsets. Note that the linked prior art for custom DSTs states verbatim that:

    Communicating with C types that contain flexible array members is an important part of this RFC.

    Conclusion

    I had much more to say here about the lifetime of the getdents() buffer, but I think this should be sufficient for now. I don't think this necessarily refutes anything (for example, whether extern types should support VLAs as a type of DST), but I wanted to mention at least one case in which alignment (necessary for allocations) is distinguished from size and field layout (particularly in the case of DSTs, VLAs, and especially struct posix_dent), and why it might be useful to distinguish these for extern types.

  22. added
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    on Feb 14, 2026
  23. added a commit that references this issue on Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-FFIArea: Foreign function interface (FFI)B-RFC-implementedBlocker: Approved by a merged RFC and implemented but not stabilized.B-unstableBlocker: Implemented in the nightly compiler and unstable.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCF-extern_types`#![feature(extern_types)]`S-tracking-needs-summaryStatus: It's hard to tell what's been done and what hasn't! Someone should do some investigation.T-langRelevant to the language teamT-typesRelevant to the types team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions