Skip to content

Commit

Permalink
Add support for already_declared enums (#125)
Browse files Browse the repository at this point in the history
This commit allows enums to use the `already_declared` attribute.

This is useful when you want to use the same enum across multiple bridge
modules.

---

For example, the following is now possible:

```rust
use some_other_bridge_module::SomeEnum;

#[swift_bridge::bridge]
mod ffi {
    #[swift_bridge(already_declared)]
    enum SomeEnum {}

    extern "Rust" {
        fn print(val: SomeEnum);
    }
}
```
  • Loading branch information
chinedufn committed Dec 15, 2022
1 parent 6a10cc2 commit 72e1759
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
228FE64A274919C600805D9E /* swift-integration-tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228FE648274919C500805D9E /* swift-integration-tests.swift */; };
22BC10F62799283100A0D046 /* SharedStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BC10F52799283100A0D046 /* SharedStruct.swift */; };
22BC10F82799A3A000A0D046 /* SharedStructAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BC10F72799A3A000A0D046 /* SharedStructAttributes.swift */; };
22BC4BBA294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BC4BB9294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift */; };
22BCAAB927A2607700686A21 /* FunctionAttributeIdentifiableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BCAAB827A2607700686A21 /* FunctionAttributeIdentifiableTests.swift */; };
22C0625328CE699D007A6F67 /* Callbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C0625228CE699D007A6F67 /* Callbacks.swift */; };
22C0625528CE6C9A007A6F67 /* CallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22C0625428CE6C9A007A6F67 /* CallbackTests.swift */; };
Expand Down Expand Up @@ -99,6 +100,7 @@
228FE649274919C600805D9E /* swift-integration-tests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "swift-integration-tests.h"; path = "swift-integration-tests/swift-integration-tests.h"; sourceTree = "<group>"; };
22BC10F52799283100A0D046 /* SharedStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStruct.swift; sourceTree = "<group>"; };
22BC10F72799A3A000A0D046 /* SharedStructAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedStructAttributes.swift; sourceTree = "<group>"; };
22BC4BB9294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedEnumAttributeTests.swift; sourceTree = "<group>"; };
22BCAAB827A2607700686A21 /* FunctionAttributeIdentifiableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionAttributeIdentifiableTests.swift; sourceTree = "<group>"; };
22C0625228CE699D007A6F67 /* Callbacks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Callbacks.swift; sourceTree = "<group>"; };
22C0625428CE6C9A007A6F67 /* CallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallbackTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -199,6 +201,7 @@
222A81EA28EB5DF800D4A412 /* PrimitiveTests.swift */,
220432EB27530AFC00BAE645 /* RustFnUsesOpaqueSwiftTypeTests.swift */,
2202BC0727B2DD1700D43CC4 /* SharedEnumTests.swift */,
22BC4BB9294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift */,
22C0AD50278ECA9E00A96469 /* SharedStructAttributeTests.swift */,
220432AE274E7BF800BAE645 /* SharedStructTests.swift */,
228FE5E62740DB6D00805D9E /* StringTests.swift */,
Expand Down Expand Up @@ -402,6 +405,7 @@
22C0AD51278ECA9E00A96469 /* SharedStructAttributeTests.swift in Sources */,
228FE5E72740DB6D00805D9E /* StringTests.swift in Sources */,
22C0625528CE6C9A007A6F67 /* CallbackTests.swift in Sources */,
22BC4BBA294B8CCD0032B8A8 /* SharedEnumAttributeTests.swift in Sources */,
228FE61227428A8D00805D9E /* OpaqueSwiftStructTests.swift in Sources */,
22FD1C562753CB3F00F64281 /* SwiftFnUsesOpaqueRustTypeTests.swift in Sources */,
228FE61027416C0300805D9E /* OpaqueRustStructTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SharedEnumAttributeTests.swift
// SwiftRustIntegrationTestRunnerTests
//
// Created by Frankie Nwafili on 12/15/22.
//

import XCTest
@testable import SwiftRustIntegrationTestRunner

/// Tests for attributes on transparent enum types.
class SharedEnumAttributeTests: XCTestCase {
/// Verify that we can call a function that uses a type that was already declared in a different bridge module.
func testSharedEnumAlreadyDeclared() throws {
XCTAssertEqual(
reflect_already_declared_enum(
AlreadyDeclaredEnumTest.Variant
),
AlreadyDeclaredEnumTest.Variant
)
}
}

9 changes: 7 additions & 2 deletions crates/swift-bridge-ir/src/bridged_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,9 +933,14 @@ impl BridgedType {
prefixed_ty_name
}
BridgedType::Foreign(CustomBridgedType::Shared(SharedType::Enum(shared_enum))) => {
let ffi_ty_name = shared_enum.ffi_name_tokens();
let ty_name = &shared_enum.name;

quote! { #ffi_ty_name }
if shared_enum.already_declared {
quote! { <super:: #ty_name as #swift_bridge_path::SharedEnum>::FfiRepr }
} else {
let ffi_ty_name = shared_enum.ffi_name_tokens();
quote! { #ffi_ty_name }
}
}
};

Expand Down
1 change: 1 addition & 0 deletions crates/swift-bridge-ir/src/bridged_type/shared_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(crate) use self::enum_variant::EnumVariant;
pub(crate) struct SharedEnum {
pub name: Ident,
pub variants: Vec<EnumVariant>,
pub already_declared: bool,
}

impl SharedEnum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,60 @@ struct __swift_bridge__$FfiSomeType __swift_bridge__$some_function(struct __swif
}
}

/// Verify that we do not re-declare an already defined enum.
mod already_declared_enum {
use super::*;

fn bridge_module_tokens() -> TokenStream {
quote! {
mod ffi {
#[swift_bridge(already_declared, swift_repr = "enum")]
enum FfiSomeEnum {}

extern "Rust" {
fn some_function(arg: FfiSomeEnum) -> FfiSomeEnum;
}
}
}
}

fn expected_rust_tokens() -> ExpectedRustTokens {
ExpectedRustTokens::ContainsManyAndDoesNotContainMany {
contains: vec![quote! {
pub extern "C" fn __swift_bridge__some_function(arg: <super::FfiSomeEnum as swift_bridge::SharedEnum>::FfiRepr) -> <super::FfiSomeEnum as swift_bridge::SharedEnum>::FfiRepr {
super::some_function(arg.into_rust_repr()).into_ffi_repr()
}
}],
does_not_contain: vec![quote! {
enum FfiSomeEnum
}],
}
}

fn expected_swift_code() -> ExpectedSwiftCode {
ExpectedSwiftCode::DoesNotContainAfterTrim("enum FfiSomeEnum")
}

fn expected_c_header() -> ExpectedCHeader {
ExpectedCHeader::ExactAfterTrim(
r#"
struct __swift_bridge__$FfiSomeEnum __swift_bridge__$some_function(struct __swift_bridge__$FfiSomeEnum arg);
"#,
)
}

#[test]
fn already_declared_enum() {
CodegenTest {
bridge_module: bridge_module_tokens().into(),
expected_rust_tokens: expected_rust_tokens(),
expected_swift_code: expected_swift_code(),
expected_c_header: expected_c_header(),
}
.test();
}
}

/// Verify that we do not re-declare the Swift struct of an already defined Rust Copy type.
/// We still generate the Rust #\[repr(C)] struct since that is private to this module.
mod already_declared_rust_copy_type_does_not_redeclare_swift {
Expand Down
4 changes: 4 additions & 0 deletions crates/swift-bridge-ir/src/codegen/generate_c_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ typedef struct {option_ffi_name} {{ bool is_some; {ffi_name} val; }} {option_ffi
header += "\n";
}
SharedTypeDeclaration::Enum(ty_enum) => {
if ty_enum.already_declared {
continue;
}

let ffi_name = ty_enum.ffi_name_string();
let ffi_tag_name = ty_enum.ffi_tag_name_string();
let option_ffi_name = ty_enum.ffi_option_name_string();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ impl SwiftBridgeModule {
&self,
shared_enum: &SharedEnum,
) -> Option<TokenStream> {
if shared_enum.already_declared {
return None;
}

let enum_name = &shared_enum.name;
let swift_bridge_path = &self.swift_bridge_path;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use crate::SwiftBridgeModule;
impl SwiftBridgeModule {
/// Generate the tokens for a shared enum.
pub(super) fn generate_shared_enum_string(&self, shared_enum: &SharedEnum) -> Option<String> {
if shared_enum.already_declared {
return None;
}

let enum_name = shared_enum.swift_name_string();
let enum_ffi_name = shared_enum.ffi_name_string();
let option_ffi_name = shared_enum.ffi_option_name_string();
Expand Down
6 changes: 6 additions & 0 deletions crates/swift-bridge-ir/src/errors/parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub(crate) enum ParseError {
StructInvalidSwiftRepr { swift_repr_attr_value: LitStr },
/// A struct was declared with an unrecognized attribute.
StructUnrecognizedAttribute { attribute: Ident },
/// An enum was declared with an unrecognized attribute.
EnumUnrecognizedAttribute { attribute: Ident },
/// There is no reason to use `swift_repr = "class"` on an empty struct.
/// It's extra overhead with no advantages.
EmptyStructHasSwiftReprClass {
Expand Down Expand Up @@ -157,6 +159,10 @@ struct {struct_name};
let message = format!(r#"Did not recognize struct attribute "{}"."#, attribute);
Error::new_spanned(attribute, message)
}
ParseError::EnumUnrecognizedAttribute { attribute } => {
let message = format!(r#"Did not recognize enum attribute "{}"."#, attribute);
Error::new_spanned(attribute, message)
}
ParseError::FunctionAttribute(fn_attrib) => match fn_attrib {
FunctionAttributeParseError::Identifiable(identifiable) => match identifiable {
IdentifiableParseError::MustBeRefSelf { fn_ident } => {
Expand Down
24 changes: 23 additions & 1 deletion crates/swift-bridge-ir/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use crate::parse::parse_enum::SharedEnumDeclarationParser;
use crate::parse::parse_extern_mod::ForeignModParser;
use crate::parse::parse_struct::SharedStructDeclarationParser;
use crate::SwiftBridgeModule;
use proc_macro2::TokenTree;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{Item, ItemMod};
use syn::{Item, ItemMod, Token};

mod parse_enum;
mod parse_extern_mod;
Expand Down Expand Up @@ -139,6 +140,27 @@ impl Parse for SwiftBridgeModuleAndErrors {
}
}

// Used to fast-forward our attribute parsing to the next attribute when we've run into an
// issue parsing the current attribute.
fn move_input_cursor_to_next_comma(input: ParseStream) {
if !input.peek(Token![,]) {
let _ = input.step(|cursor| {
let mut current_cursor = *cursor;

while let Some((tt, next)) = current_cursor.token_tree() {
match &tt {
TokenTree::Punct(punct) if punct.as_char() == ',' => {
return Ok(((), current_cursor));
}
_ => current_cursor = next,
}
}

Ok(((), current_cursor))
});
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
121 changes: 119 additions & 2 deletions crates/swift-bridge-ir/src/parse/parse_enum.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::bridged_type::{EnumVariant, SharedEnum, StructFields};
use crate::errors::ParseErrors;
use crate::errors::{ParseError, ParseErrors};
use crate::parse::move_input_cursor_to_next_comma;
use proc_macro2::Ident;
use syn::parse::{Parse, ParseStream};
use syn::ItemEnum;

pub(crate) struct SharedEnumDeclarationParser<'a> {
Expand All @@ -9,12 +12,74 @@ pub(crate) struct SharedEnumDeclarationParser<'a> {
pub errors: &'a mut ParseErrors,
}

enum EnumAttr {
AlreadyDeclared,
Error(EnumAttrParseError),
}

enum EnumAttrParseError {
UnrecognizedAttribute(Ident),
}

#[derive(Default)]
struct EnumAttribs {
already_declared: bool,
}

struct ParsedAttribs(Vec<EnumAttr>);
impl Parse for ParsedAttribs {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(ParsedAttribs(vec![]));
}

let opts = syn::punctuated::Punctuated::<_, syn::token::Comma>::parse_terminated(input)?;

Ok(ParsedAttribs(opts.into_iter().collect()))
}
}

impl Parse for EnumAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;

let attr = match key.to_string().as_str() {
"already_declared" => EnumAttr::AlreadyDeclared,
_ => {
move_input_cursor_to_next_comma(input);
EnumAttr::Error(EnumAttrParseError::UnrecognizedAttribute(key))
}
};

Ok(attr)
}
}

impl<'a> SharedEnumDeclarationParser<'a> {
pub fn parse(self) -> Result<SharedEnum, syn::Error> {
let item_enum = self.item_enum;

let mut variants = vec![];

let mut attribs = EnumAttribs::default();
for attr in item_enum.attrs {
let sections: ParsedAttribs = attr.parse_args()?;

for attr in sections.0 {
match attr {
EnumAttr::AlreadyDeclared => {
attribs.already_declared = true;
}
EnumAttr::Error(err) => match err {
EnumAttrParseError::UnrecognizedAttribute(attribute) => {
self.errors
.push(ParseError::EnumUnrecognizedAttribute { attribute });
}
},
}
}
}

for v in item_enum.variants {
let variant = EnumVariant {
name: v.ident,
Expand All @@ -26,6 +91,7 @@ impl<'a> SharedEnumDeclarationParser<'a> {
let shared_enum = SharedEnum {
name: item_enum.ident,
variants,
already_declared: attribs.already_declared,
};

Ok(shared_enum)
Expand All @@ -35,7 +101,8 @@ impl<'a> SharedEnumDeclarationParser<'a> {
#[cfg(test)]
mod tests {
use crate::bridged_type::StructFields;
use crate::test_utils::parse_ok;
use crate::errors::ParseError;
use crate::test_utils::{parse_errors, parse_ok};
use quote::{quote, ToTokens};

/// Verify that we can parse an enum with no variants.
Expand Down Expand Up @@ -105,4 +172,54 @@ mod tests {
_ => panic!(),
}
}

/// Verify that we can parse the `#[swift_bridge(already_declared)`] attribute.
#[test]
fn already_declared_attribute() {
let tokens = quote! {
#[swift_bridge::bridge]
mod ffi {
#[swift_bridge(already_declared)]
enum SomeEnum {}
}
};

let module = parse_ok(tokens);

assert_eq!(module.types.types().len(), 1);

let ty = &module.types.types()[0].unwrap_shared_enum();
assert!(ty.already_declared);
}

/// Verify that we return an error if an attribute isn't recognized.
#[test]
fn error_if_attribute_unrecognized() {
let tokens = quote! {
#[swift_bridge::bridge]
mod ffi {
#[swift_bridge(unrecognized, invalid_attribute = "hi")]
enum SomeEnum{
Variant
}
}
};

let errors = parse_errors(tokens);

assert_eq!(errors.len(), 2);

match &errors[0] {
ParseError::EnumUnrecognizedAttribute { attribute } => {
assert_eq!(&attribute.to_string(), "unrecognized");
}
_ => panic!(),
};
match &errors[1] {
ParseError::EnumUnrecognizedAttribute { attribute } => {
assert_eq!(&attribute.to_string(), "invalid_attribute");
}
_ => panic!(),
};
}
}

0 comments on commit 72e1759

Please sign in to comment.