Skip to content

C# Bincode: optional C-style enums use reference-type helpers #131

Description

@mhedgpeth

The C# Bincode generator chooses reference-type option helpers for an Option<T> where T is emitted as a C# enum. C# enums are value types, so the generated calls violate the helpers' where T : class constraints.

Observed with facet_generate 0.19.0. The same helper-selection logic is present on main at 76a0246604307b92f4d9daa15238e405f85d17c2 (source inspection; I have not run this standalone reproducer against main).

Minimal example

Generate C# with BincodePlugin for these types:

use facet::Facet;
use facet_generate::{
    generation::{bincode::BincodePlugin, csharp},
    reflection::RegistryBuilder,
};

#[derive(Facet)]
#[repr(C)]
enum ContactGroup {
    Align,
    Partner,
}

#[derive(Facet)]
struct Filter {
    group: Option<ContactGroup>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let registry = RegistryBuilder::new().add_type::<Filter>()?.build()?;
    csharp::Installer::new("Example", "generated")
        .plugin(BincodePlugin)
        .generate(&registry)?;
    Ok(())
}

The generated ContactGroup is a C# enum, and the property is nullable (ContactGroup?). However, the generated Bincode calls use the reference helpers (namespace simplified):

FacetHelpers.SerializeOptionRef(Group, serializer,
    (item, s) => Example.ContactGroupBincode.Serialize(item, s));

var group = FacetHelpers.DeserializeOptionRef(deserializer,
    d => Example.ContactGroupBincode.Deserialize(d));

Those helpers constrain T to class; compiling the generated C# therefore fails for the enum value type.

Expected:

FacetHelpers.SerializeOption(Group, serializer,
    (item, s) => Example.ContactGroupBincode.Serialize(item, s));

var group = FacetHelpers.DeserializeOption(deserializer,
    d => Example.ContactGroupBincode.Deserialize(d));

The existing value helpers use where T : struct and preserve None as null.

Likely cause and fix

option_serialize_helper / option_deserialize_helper call is_csharp_value_type, which recognizes primitives and tuples but does not resolve Format::TypeName against the generated C-style enum set. The serialization/deserialization expression writers already have c_style_enums available.

Use that type information when selecting option helpers, for both serialization and deserialization. Keep reference helpers for enums emitted as record hierarchies and for other reference types.

Suggested regression coverage

  • Compile generated C# for an optional C-style enum.
  • Round-trip None, Some(Align) (the first/zero-valued variant), and Some(Partner) between Rust and C#.
  • Cover optional enums inside collections as well as direct fields.
  • Retain coverage for optional record-based enums and reference types.

Downstream impact

We encountered this in a Windows app's optional contact-group filter. A previous downstream workaround removed the reference-helper constraint and returned default; that incorrectly collapsed None into the first enum value. That silent corruption was caused by our workaround, not by upstream's unmodified output. Our current workaround selects the correct nullable-value helper, but this belongs in the generator so consumers need no output patching.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions