jam1garner / jam1garner/binrw

Symmetric `calc` behavior for `BinRead` and `BinWrite`

Open
#364 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Rust
Stars
853
Forks
56
PR merge metrics
No merged PRs in 30d

Description

When deriving `binrw::BinRead`, `#[br(calc = ...)]` allows a field value to be synthesized without consuming bytes from the input stream. This makes it possible to create intermediate values that can be referenced by later fields during parsing.

When deriving `binrw::BinWrite`, `#[bw(calc = ...)]` behaves differently. Although it synthesizes a value, that value still participates in serialization and is written to the output stream. There is currently no equivalent mechanism for creating a calculated value that exists only for use by later write-time attributes without producing output:

* `#[br(calc = ...)]` synthesizes a value without consuming bytes.
* `#[bw(calc = ...)]` synthesizes a value and writes it to the output stream.
* `#[bw(ignore, calc = ...)]` is not allowed ("conflicting read mode keyword").

This makes `calc` asymmetric between reading and writing. Users who need to compute a value and reuse it during serialization must either inline the calculation at every use site or implement a no-op `binrw::BinWrite` implementation for the synthesized type.

## Workaround: Inline calculation

In this example, `header.serialization_format()` is called twice.

```rust
#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct SaveGameFile {
pub header: FSaveGameHeader,

#[brw(if(header.serialization_format().property_tag_complete_type_name))]
#[br(temp, assert(spacer == 0))]
#[bw(calc(0))]
spacer: u8,

#[brw(args(header.serialization_format()))]
pub properties: TaggedProperties,

#[br(temp, assert(footer == 0))]
#[bw(calc(0))]
footer: u32,
}
```

## Workaround: No-op `BinWrite` implementation

Another workaround is to implement `binrw::BinWrite` for the synthesized type and make it intentionally produce no output:

```rs
impl binrw::BinWrite for SerializationFormat {
type Args<'a> = ();

fn write_options(
&self,
_writer: &mut W,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<()> {
// This is a synthetic type, do not write anything
Ok(())
}
}

#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct SaveGameFile {
pub header: FSaveGameHeader,

#[brw(calc = header.serialization_format())]
#[br(temp)]
// #[bw(ignore)] cannot be combined with calc, so SerializationFormat implements a no-op BinWrite
format: SerializationFormat,

...
}
```

However, this changes the behavior of `SerializationFormat` globally and makes it possible to accidentally use a type with a surprising serialization implementation elsewhere.

## Desired solution:

Support combining `ignore` and `calc` for derived `BinWrite`.

```rs
#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct SaveGameFile {
pub header: FSaveGameHeader,

#[brw(calc = header.serialization_format())]
#[br(temp)]
#[bw(ignore)]
format: SerializationFormat,

...
}
```

If `br(ignore)` were aliased to `br(temp)`, this could be simplified even further to:
```rust
#[brw(ignore, calc = header.serialization_format())]
format: SerializationFormat,
```

Supporting such a pattern would improve the symmetry between `BinRead` and `BinWrite` and could reduce duplication in more complex serializers.

Contributor guide

Open the contributing guide

Research direction

No source files or tests are named. Start by locating the derive handling for BinWrite's calc and ignore attributes, then compare it with BinRead's handling; done means allowing the two BinWrite attributes together without serializing the calculated field, with coverage for the demonstrated pattern.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
tooling
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.