Repository: dtolnay/cxx (rust)

Issue:
Extern C-like enums
The enum support from #170 doesn't apply to interop with existing enums for which C++ is the source of truth.

I wonder if we could handle this use case as follows:

```rust
mod ffi {
    enum SmallPrime {
        Two = 2,
        Three = 3,
        Five = 5,
    }

    extern "C" {
        type SmallPrime;
    }
}
```

where we ask that the enum variants be provided just like in #170, but if the same type name is also an extern "C" type then we don't emit a C++ enum class definition, but instead emit static assertions on the C++ side asserting that the cxx bridge discriminants are all the same as the existing C++ enum's values.

Pull request description:
Support enums defined in C++ code.
This allows listing an enum in an extern "C" block as well as in the shared block.  In this case, we do not emit the C++ declaration of the enum but instead emit static assertions that it has the values that we expect.

Do you know a good way to test the assertions?  The ui tests don't seem to compile the generated C++ files, so I'm not sure how to do it.

I'm also considering refactoring the discriminant generation code somewhere now that it's mostly duplicated in three different places.

Closes #177.

Patch:
diff --git a/gen/src/write.rs b/gen/src/write.rs
index 8ce38138..3c059a71 100644
--- a/gen/src/write.rs
+++ b/gen/src/write.rs
@@ -66,7 +66,11 @@ pub(super) fn gen(
             }
             Api::Enum(enm) => {
                 out.next_section();
-                write_enum(out, enm);
+                if types.cxx.contains(&enm.ident) {
+                    check_enum(out, enm);
+                } else {
+                    write_enum(out, enm);
+                }
             }
             Api::RustType(ety) => {
                 if let Some(methods) = methods_for_type.get(&ety.ident) {
@@ -373,6 +377,27 @@ fn write_enum(out: &mut OutFile, enm: &Enum) {
     writeln!(out, "}};");
 }
 
+fn check_enum(out: &mut OutFile, enm: &Enum) {
+    writeln!(
+        out,
+        "static_assert(sizeof({}) == sizeof(uint32_t), \"incorrect size\");",
+        enm.ident
+    );
+    let mut prev_discriminant = None;
+    for variant in &enm.variants {
+        let discriminant = variant
+            .discriminant
+            .unwrap_or_else(|| prev_discriminant.map_or(0, |n| n + 1));
+        writeln!(
+            out,
+            "static_assert(static_cast<uint32_t>({}::{}) == {},
+              \"disagrees with the value in #[cxx::bridge]\");",
+            enm.ident, variant.ident, discriminant,
+        );
+        prev_discriminant = Some(discriminant);
+    }
+}
+
 fn write_exception_glue(out: &mut OutFile, apis: &[Api]) {
     let mut has_cxx_throws = false;
     for api in apis {
diff --git a/macro/src/expand.rs b/macro/src/expand.rs
index e67902b3..d8810dcb 100644
--- a/macro/src/expand.rs
+++ b/macro/src/expand.rs
@@ -44,7 +44,11 @@ fn expand(namespace: &Namespace, ffi: ItemMod, apis: &[Api], types: &Types) -> T
             Api::Include(_) | Api::RustType(_) => {}
             Api::Struct(strct) => expanded.extend(expand_struct(strct)),
             Api::Enum(enm) => expanded.extend(expand_enum(enm)),
-            Api::CxxType(ety) => expanded.extend(expand_cxx_type(ety)),
+            Api::CxxType(ety) => {
+                if !types.enums.contains_key(&ety.ident) {
+                    expanded.extend(expand_cxx_type(ety));
+                }
+            }
             Api::CxxFunction(efn) => {
                 expanded.extend(expand_cxx_function_shim(namespace, efn, types));
             }
diff --git a/syntax/types.rs b/syntax/types.rs
index 02249e2e..58005139 100644
--- a/syntax/types.rs
+++ b/syntax/types.rs
@@ -61,15 +61,18 @@ impl<'a> Types<'a> {
                 }
                 Api::Enum(enm) => {
                     let ident = &enm.ident;
-                    if type_names.insert(ident) {
-                        enums.insert(ident.clone(), enm);
-                    } else {
+                    // We allow declaring the same type as a shared enum and as a Cxxtype, as this
+                    // means not to emit the C++ enum definition.
+                    if !type_names.insert(ident) && !cxx.contains(ident) {
                         duplicate_name(cx, enm, ident);
                     }
+                    enums.insert(ident.clone(), enm);
                 }
                 Api::CxxType(ety) => {
                     let ident = &ety.ident;
-                    if !type_names.insert(ident) {
+                    // We allow declaring the same type as a shared enum and as a Cxxtype, as this
+                    // means not to emit the C++ enum definition.
+                    if !type_names.insert(ident) && !enums.contains_key(ident) {
                         duplicate_name(cx, ety, ident);
                     }
                     cxx.insert(ident);


Test patch:
diff --git a/tests/ffi/lib.rs b/tests/ffi/lib.rs
index 0162c9b8..16846fbd 100644
--- a/tests/ffi/lib.rs
+++ b/tests/ffi/lib.rs
@@ -84,6 +84,15 @@ pub mod ffi {
         fn set2(&mut self, n: usize) -> usize;
     }
 
+    extern "C" {
+        type COwnedEnum;
+    }
+
+    enum COwnedEnum {
+        CVal1,
+        CVal2,
+    }
+
     extern "Rust" {
         type R;
         type R2;
diff --git a/tests/ffi/tests.h b/tests/ffi/tests.h
index d3b7d38d..795b4a9f 100644
--- a/tests/ffi/tests.h
+++ b/tests/ffi/tests.h
@@ -23,6 +23,11 @@ private:
   std::vector<uint8_t> v;
 };
 
+enum COwnedEnum {
+  CVal1,
+  CVal2,
+};
+
 size_t c_return_primitive();
 Shared c_return_shared();
 rust::Box<R> c_return_box();