Skip to content

Tracking issue for unsupported_calling_conventions (cdecl, stdcall, fastcall) #137018

Open

Description

@RalfJung

View all comments

This is a tracking issue for the unsupported_calling_conventions lint. This lint warns against

  • Using extern "cdecl" on non-x86-32 targets. Use extern "C" instead, there is no difference in behavior.
  • Using extern "stdcall" or extern "fastcall" on non-x86-32 Windows targets. These ABIs are only defined for x86-32.
    • If you want to use "stdcall on x86-32-windows, C everywhere else", then use extern "system".
    • Otherwise, if you really need the same function both on x86-32 and on other targets but with a different ABI, consider using the fn_abi crate. Also we'd be interested to hear about your use-case, so please leave a comment below.

Implementation history:

Timeline:

  • This was introduced with Rust 1.89.
  • There's no timeline yet for when this will show up in cargo's future-compat reports, or be turned into a hard error. Since windows-sys needs an update to v0.60 to avoid the lint, we'll have to move really slowly here.

Original issue description

We generally accept stdcall on non-x86-32 Windows targets (while rejecting it on other non-x86-32 targets). See the discussion about this at #86231 (comment) and #86231 (comment). However, @ChrisDenton points out that the raw-dylib feature is more strict here. That's an inconsistency that we should probably fix?

@ChrisDenton can you give an example of what exactly is being rejected by raw-dylib here?

2025-05-05: See here for the current latest proposal.

Cc @workingjubilee @rust-lang/lang @nagisa @petrochenkov

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 14, 2025
  2. added
    O-windowsOperating system: Windows
    A-ABIArea: Concerning the application binary interface (ABI)
    C-bugCategory: This is a bug.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    T-langRelevant to the language team
    on Feb 14, 2025
  3. ChrisDenton commented on Feb 14, 2025

    @ChrisDenton
    Member

    It's just generally more strict. As far as I recall this is intentional, cc @dpaoliello. For example:

    #[link(name = "library", kind = "raw-dylib")]
    extern "cdecl" { // or stdcall
        fn test();
    }

    This is rejected on x86_64-pc-windows-msvc and aarch64-pc-windows-msvc. But remove kind = "raw-dylib" and it works.

  4. RalfJung commented on Feb 14, 2025

    @RalfJung
    MemberAuthor
  5. Aditya-PS-05 commented on Feb 14, 2025

    @Aditya-PS-05
  6. RalfJung commented on Feb 14, 2025

    @RalfJung
    MemberAuthor

    @Aditya-PS-05 currently this issue is about discussing what to do next; there is no agreed-upon plan for what to do yet.


    Reading the past comments, it seems like ~noone argued in favor of the status quo. So maybe the best next step is to have a FCW lint against

    • stdcall, fastcall on everything but 32-bit x86
    • vectorcall on everything but x86 (32bit or 64bit)
    • EDIT: cdecl on everything but 32-bit x86

    This should probably re-introduce the lint that was removed in #129935.

    @ChrisDenton also mentioned cdecl above; I did not have that on my radar yet. Currently, that one is accepted everywhere. Which targets should it support?

    Either way, raw-dylib shouldn't have its own "does this ABI make sense" logic (currently located here); it should call is_abi_supported like everything else. (But that function might return a three-state value to indicate "not really but for backwards compatibility we accept this in some cases; and raw-dylib can then make a hard error rather than an FCW for those.)

  7. Aditya-PS-05 commented on Feb 14, 2025

    @Aditya-PS-05
  8. ChrisDenton commented on Feb 14, 2025

    @ChrisDenton
  9. dpaoliello commented on Feb 14, 2025

    @dpaoliello
    Contributor

    It's just generally more strict. As far as I recall this is intentional, cc @dpaoliello.

    Yes, it was intentional, although I would describe it more as "let's start restrictive and we'll loosen things later" rather than "this is the absolute correct behavior": #86419 (comment)

    Either way, raw-dylib shouldn't have its own "does this ABI make sense" logic (currently located here); it should call is_abi_supported like everything else.

    raw-dylib has its own logic for historical reasons: primarily that it wasn't supported for all calling conventions or all arches for a while, hence its own logic to filter to what it did support.

    Personally, I'm going to bow out of this discussion. I previously said that having this behavior would cause a proliferation of cfg_attr attributes without a huge benefit to safety or correctness but was told that #87678 would eventually be an error and so this would be moot: https://rust-lang.zulipchat.com/#narrow/channel/233931-t-compiler.2Fmajor-changes/topic/Add.20import_name_type.20parameter.20to.20.23.5Blink.5D.20compiler-team.23525

  10. 39 remaining items

  11. RalfJung commented on Jul 1, 2025

    @RalfJung
    MemberAuthor

    Does system coincide with stdcall on ARM64 and x64 machines?

    There's no such thing as stdcall on ARM64, is there...?
    Or do you mean, "the thing that MSVC does when you use stdcall on ARM64"? AFAIK, it just ignores the stdcall, silently, and uses the normal C ABI instead. So in that sense "system" is the same thing, yes.

  12. pronebird commented on Jul 1, 2025

    @pronebird

    Does system coincide with stdcall on ARM64 and x64 machines?

    There's no such thing as stdcall on ARM64, is there...? Or do you mean, "the thing that MSVC does when you use stdcall on ARM64"? AFAIK, it just ignores the stdcall, silently, and uses the normal C ABI instead. So in that sense "system" is the same thing, yes.

    I have no idea to be frank. I am only exposed to calling conventions as a WinAPI user.

    I'd need to boot and see what #define WINAPI translates to on ARM64 Windows, however I asked GitHub AI just for giggles and it printed this table for me:

    Summary Table:

    Architecture WINAPI expands to Calling Convention Used
    x86 __stdcall __stdcall
    x64 (nothing) Microsoft x64 ABI (register-based)
    ARM/ARM64 (nothing) Standard Windows ARM/ARM64 ABI

    So that coincides with what you said and I assume system maps to the same conventions on each architecture. Also windows crate uses system which is good news.

  13. workingjubilee commented on Jul 1, 2025

    @workingjubilee
    Member

    Why even keep extern "cdecl" at all if it's the same as "C" in 32-bit windows (the only place where it's now possible to use it)?

    For context, I'm impacted by this change (found out through clippy nightly lints) as I'm maintaining some FFI bindings for a library that uses _cdecl in the C headers. I find this change makes it a bit confusing to figure out what the "correct" way to bind those APIs is.

    Maybe we can make the messaging more precise here?

    If you are using extern "cdecl" then you can always use extern "C" instead, i.e. it's sound to do the mem::transmute between those ABIs, unconditionally.

  14. RalfJung commented on Jul 1, 2025

    @RalfJung
    MemberAuthor

    The error already explicitly says that you can replace "cdecl" by "C" unconditionally.

    "cdecl" is an ABI that only exists on x86, which is why we now warn if you use it anywhere else.

  15. changed the title [-]Tracking issue for unsupported_calling_conventions[/-] [+]Tracking issue for unsupported_calling_conventions (cdecl, stdcall, fastcall)[/+] on Jul 21, 2025
  16. kennykerr commented on Jul 31, 2025

    @kennykerr
  17. RalfJung commented on Aug 1, 2025

    @RalfJung
    Author
  18. MauriceKayser commented on Apr 2, 2026

    @MauriceKayser

    RalfJung on 2025-05-05

    I was not able to find people complaining about [...] the ABI "fastcall on x86-32, C everywhere else" [...] on Windows [...], that could no longer be easily expressed

    I hope this is a good place for feedback, I would like to present the following use case:

    On Windows, when interfacing with an MSVC compiled C++ dynamic library that exports C functions to create and free objects of a C++ class, and the class has virtual functions, they will be thiscall on x86-32, and C otherwise:

    // Exported interface
    class Interface
    {
    public:
        // Virtual functions
        virtual ~Interface() = default;
        virtual int get() const = 0;
        virtual void set(int value) = 0;
    };
    
    // Example implementation of `Interface`
    class Class : public Interface
    {
    private:
        int m_data;
    
    public:
        Class() : m_data(0) {}
        ~Class() override = default;
    
        int get() const override { return this->m_data; }
        void set(int value) override { this->m_data = value; }
    };
    
    // C exports
    extern "C" __declspec(dllexport) Interface* New() { return new Class(); }
    extern "C" __declspec(dllexport) void Free(Interface* instance) { delete instance; }
    C++ compilation script if you want to try, should be in a subdirectory of the Rust project directory structure, e.g. "rust-proj\cpp-code\"
    @echo off
    setlocal enabledelayedexpansion
    
    :: Get architecture from argument (default to both)
    set ARCH=%1
    
    if "%ARCH%"=="" (
        echo Building for both x86 and x64...
        call :build_arch x86
        call :build_arch x64
        goto :end
    )
    
    call :build_arch %ARCH%
    goto :end
    
    :build_arch
    set BUILD_ARCH=%1
    
    if "%BUILD_ARCH%"=="x86" (
        set VS_ARCH=x86
        set OUT_DIR=.\bin\x86
        set RUST_TARGET=i686-pc-windows-msvc
    ) else if "%BUILD_ARCH%"=="x64" (
        set VS_ARCH=x64
        set OUT_DIR=.\bin\x64
        set RUST_TARGET=x86_64-pc-windows-msvc
    ) else (
        echo Invalid architecture: %BUILD_ARCH%
        echo Usage: build.bat [x86^|x64]
        exit /b 1
    )
    
    echo.
    echo ========================================
    echo Building for %BUILD_ARCH%...
    echo ========================================
    echo.
    
    :: Find Visual Studio installation
    set VSWHERE="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
    if not exist %VSWHERE% (
        echo Error: vswhere.exe not found. Please install Visual Studio 2017 or later.
        exit /b 1
    )
    
    for /f "usebackq tokens=*" %%i in (`%VSWHERE% -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do (
        set VS_PATH=%%i
    )
    
    if not defined VS_PATH (
        echo Error: Visual Studio installation not found.
        exit /b 1
    )
    
    :: Load Visual Studio environment
    set VCVARSALL="%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat"
    if not exist %VCVARSALL% (
        echo Error: vcvarsall.bat not found at %VCVARSALL%
        exit /b 1
    )
    
    echo Loading Visual Studio environment for %VS_ARCH%...
    call %VCVARSALL% %VS_ARCH%
    
    :: Create output directory
    if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
    
    :: Build
    echo Compiling main.cpp...
    cl.exe /LD /EHsc /O2 /Fe:"%OUT_DIR%\cpp.dll" main.cpp
    
    if %ERRORLEVEL% EQU 0 (
        echo.
        echo Build successful: %OUT_DIR%\cpp.dll
    
        :: Copy to Rust target directories
        set RUST_DEBUG_DIR=..\target\%RUST_TARGET%\debug
        set RUST_RELEASE_DIR=..\target\%RUST_TARGET%\release
    
        if not exist "!RUST_DEBUG_DIR!" mkdir "!RUST_DEBUG_DIR!"
        copy /Y "%OUT_DIR%\cpp.dll" "!RUST_DEBUG_DIR!\cpp.dll" >nul
        echo Copied to !RUST_DEBUG_DIR!\cpp.dll
    
        if not exist "!RUST_RELEASE_DIR!" mkdir "!RUST_RELEASE_DIR!"
        copy /Y "%OUT_DIR%\cpp.dll" "!RUST_RELEASE_DIR!\cpp.dll" >nul
        echo Copied to !RUST_RELEASE_DIR!\cpp.dll
        echo.
    ) else (
        echo.
        echo Build failed!
        exit /b 1
    )
    
    exit /b 0
    
    :end
    endlocal

    The following code does compile for i686-pc-windows-msvc, but does not for x86_64-pc-windows-msvc, because thiscall can not be used in struct Vtbl:

    // Simple test
    fn main() {
        let mut instance = InterfaceHandle::new();
        println!("Data: {}", instance.get()); // Data: 0
        instance.set(2);
        println!("Data: {}", instance.get()); // Data: 2
    }
    
    // C++ type definitions
    struct InterfaceHandle(*mut Interface);
    
    impl InterfaceHandle {
        fn new() -> Self { unsafe { Self(New()) } }
        fn get(&self) -> i32 { unsafe { ((*(*self.0).vtbl).get)(self.0) } }
        fn set(&mut self, value: i32) { unsafe { ((*(*self.0).vtbl).set)(self.0, value) } }
    }
    
    impl Drop for InterfaceHandle {
        fn drop(&mut self) { unsafe { Free(self.0) } }
    }
    
    #[repr(C)]
    struct Interface {
        vtbl: *const Vtbl,
    }
    
    #[repr(C)]
    struct Vtbl {
        destructor: extern "thiscall" fn(*mut Interface),
        get: extern "thiscall" fn(*const Interface) -> i32,
        set: extern "thiscall" fn(*mut Interface, i32),
    }
    
    #[link(name = "cpp", kind = "raw-dylib")]
    unsafe extern "C" {
        fn New() -> *mut Interface;
        fn Free(instance: *mut Interface);
    }

    The only option I see currently is duplicating the function declarations in struct Vtbl, e.g. by using a macro:

    macro_rules! vtbl {
        ($name:ident { $(($fn_name:ident: $($fn_type:tt)+)),* $(,)? }) => {
            #[repr(C)]
            struct $name { $(
                #[cfg(target_arch = "x86")]
                $fn_name: extern "thiscall" $($fn_type)+,
                #[cfg(not(target_arch = "x86"))]
                $fn_name: extern "C" $($fn_type)+,
            )* }
        };
    }
    
    vtbl!(Vtbl {
        (destructor: fn(*mut Interface)),
        (get: fn(*const Interface) -> i32),
        (set: fn(*mut Interface, i32)),
    });

    At least for Windows, one of the following options would be preferable:

    1. The calling convention thiscall becomes C if arch != x86-32
    2. A class calling convention becoming thiscall on x86-32, C elsewhere. Similar to system becoming stdcall on x86-32 and C elsewhere

    To mimic MSVCs behaviour of ignoring explicit calling conventions on non-x86-32 targets (correct me if I am wrong), the first option would probably be the most ergonomic, as that could extend to other calling conventions like fastcall, etc. without having to invent new ones like system and class.

  19. RalfJung commented on Apr 2, 2026

    @RalfJung
    MemberAuthor

    Thanks for the report! This issue is primarily about cdecl, stdcall, fastcall... not thiscall. It's related to this issue in the sense that you're basically asking to add the behavior to thiscall that we are in the process of removing from stdcall/fastcall.

    I'm not sure what a coherent design would look like here if we give thiscall fallback behavior. Does this mean stdcall and system become synonyms? That seems odd as well.

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-ABIArea: Concerning the application binary interface (ABI)C-bugCategory: This is a bug.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCI-lang-radarItems that are on lang's radar and will need eventual work or consideration.O-windowsOperating system: WindowsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions