Skip to content
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

Adding windows pointer/integer data types #254

Open
thomasantony opened this issue Aug 15, 2020 · 6 comments
Open

Adding windows pointer/integer data types #254

thomasantony opened this issue Aug 15, 2020 · 6 comments
Labels
help wanted windows Issues that manifest on Windows

Comments

@thomasantony
Copy link

thomasantony commented Aug 15, 2020

I have been trying to access a 3rd-party win32 API using cxx. One of the first things I am coming across is that the library contains extensive use of windows-specific type aliases like DWORD, WPARAM, LPARAM, HINSTANCE etc. most of which are equivalent to an unsigned integer of some size (u32 in case of 32-bit windows). Now obviously, since cxx is unaware of this, I am unable to pass or return these by value.

If I try to use u32 inside of the extern "C" block, I get errors like:

lib.rs.cc(10): error C2440: 'initializing': cannot convert from 'DWORD (__thiscall MyClass::* )(DWORD)' to 'uint32_t (__thiscall MyClass:
:* )(uint32_t)'
lib.rs.cc(10): note: Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
exit code: 2

Is there any workaround to this to make cxx "aware" of that these types are interchangeable with unsigned integers? I believe most of these are defined in minwindef.h in the windows SDK.

@thomasantony thomasantony changed the title Adding windows integer types Adding windows pointer/integer data types Aug 15, 2020
@dtolnay
Copy link
Owner

dtolnay commented Aug 27, 2020

I've started looking into this. There are some surprises for me (having never programmed for Windows)!

It looks like in my Linux environment both gcc and clang consider unsigned long to be the exact same type as the equivalently sized type from cstdint:

#include <cstdint>

static_assert(sizeof(unsigned long) == sizeof(uint64_t), ""); // passes

void f(unsigned long) {}
//void f(uint64_t) {} // not allowed

int main() {
  void (*f$)(uint64_t) = f; // okay
  (void)f$;
}

Whereas from toying around in https://msvc.godbolt.org it looks like MSVC considers them distinct types. static_assert(sizeof(unsigned long) == sizeof(uint32_t)) passes but we still get:

error C2440: 'initializing': cannot convert from 'void (__cdecl *)(unsigned long)' to 'void (__cdecl *)(uint32_t)'

Confusingly, it does appear to consider unsigned int to be the same type as uint32_t.

That means we're likely to have to introduce some Rust type that the code generator recognizes as equivalent to unsigned long (c_ulong in Rust, going by the terminology in std::os::raw and the libc crate).

For the minwindef.h types in particular, ideally cxx::ExternType would support this use case.

unsafe impl ExternType for DWORD {
    type Id = type_id!("DWORD");
}

but it doesn't quite yet, because we don't support passing them by value yet even with an ExternType impl present. I expect that restriction will be lifted with some more design work.

  error[cxxbridge]: passing C++ type by value is not supported
     ┌─ src/main.rs:19:17

  19 │         fn doit(d: DWORD);
     │                 ^^^^^^^^ passing C++ type by value is not supported

For now I guess the only immediate way to unblock signatures that involve DWORD would be for you to write some wrappers expressed in terms of uint32_t which perform (probably implicit) casts to call the original function, and only bind the wrappers from cxx, but this is obviously not great.

@dtolnay dtolnay added the windows Issues that manifest on Windows label Aug 30, 2020
@adetaylor
Copy link
Collaborator

Since the comment above, cxx has gained the ability to do this:

unsafe impl ExternType for DWORD {
      type Id = cxx::type_id!("DWORD");
      type Kind = cxx::kind::Trivial; // the new bit!
}

This isn't quite perfect though.

  • say type DWORD = std::os::raw::c_ulong;, in which case the orphan rule prevents us from implementing ExternType. Or,
  • we say #[repr(transparent)] struct DWORD(std::os::raw::c_ulong); which should work, but will be a bit awkward to use

(I didn't know about #[repr(transparent)] until I started writing this comment... it sounds like what we need... but to keep cxx happy we might need to do some different repr things).

I'm going to try the latter approach until a better plan occurs to me!

@adetaylor
Copy link
Collaborator

The newtype wrapper approach works (i.e. the second bullet above plus an implementation of ExternType for that type, with cxx::kind::Trivial). It's not especially nice due to the need to convert types to and from that newtype wrapper, but as a workaround until/unless these types come to b natively supported in cxx.

@adetaylor
Copy link
Collaborator

Specifically here's what I did, for others who end up needing to do this before/unless support lands natively in cxx.

typedef unsigned long c_ulong; // etc.

because cxx can't cope with C++ type names containing a space. (And who can blame it).

macro_rules! ctype_wrapper {
    ($r:ident, $c:expr) => {
        /// Newtype wrapper for a `$c`
        #[derive(Debug,Eq,Clone,PartialEq,Hash)]
        #[allow(non_camel_case_types)]
        #[repr(transparent)]
        pub struct $r(pub ::std::os::raw::$r);

        unsafe impl autocxx_engine::cxx::ExternType for $r {
            type Id = autocxx_engine::cxx::type_id!($c);
            type Kind = autocxx_engine::cxx::kind::Trivial;
        }
    };
}

ctype_wrapper!(c_ulong, "c_ulong");
ctype_wrapper!(c_long, "c_long");
ctype_wrapper!(c_ushort, "c_ushort");
ctype_wrapper!(c_short, "c_short");
ctype_wrapper!(c_uint, "c_uint");
ctype_wrapper!(c_int, "c_int");
ctype_wrapper!(c_uchar, "c_uchar");
ctype_wrapper!(c_char, "c_char");

There may be a way to get rid of the second parameter using stringify! but the deep magic done by cxx::type_id seems to conflict. I think. This is my first ever macro_rules! use...

Then in the cxx::bridge mod simply type c_ulong = my_crate::c_ulong;. And then use my_crate::c_ulong(3) instead of 3 when calling C++ APIs that take an unsigned long. Not quite ideal but it works for now.

@dtolnay
Copy link
Owner

dtolnay commented Dec 18, 2020

Related to supporting those without a wrapper: #529.

@dpaoliello
Copy link

I'm not sure that #529 really helps here: it would allow the cxx crate itself to define these aliases but wouldn't allow dependents to define them due to the orphan rule.

I'm assuming that checking the Ids in verify_extern_type is designed to ensure that all uses of the same Rust type generate the same C++ name to avoid authoring mistakes?

If so, then perhaps we could add an attribute to indicate to the cxx crate that the renaming was intentional:

#[cxx::bridge]
mod ffi {
    extern "C++" {
        #[renamed]
        type DWORD = u32;
        fn UseDWord(value: DWORD);
    }
}

Applying that attribute would cause cxxbridge-macro to use a different function that didn't check the Id:

pub fn verify_extern_type_renamed<T: ExternType>() {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted windows Issues that manifest on Windows
Projects
None yet
Development

No branches or pull requests

4 participants