Skip to content

refactor(bindings/dotnet): deduplicate FFI helpers and fix presigned header layout - #8344

Open
Fatorin wants to merge 7 commits into
apache:mainfrom
Fatorin:dotnet-reduce-duplication
Open

Fatorin wants to merge 7 commits into
apache:mainfrom
Fatorin:dotnet-reduce-duplication

Conversation

@Fatorin

@Fatorin Fatorin commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

None.

Rationale for this change

The .NET binding repeats the same FFI plumbing in several places, such as C string release, option payload build and free, and stream handle validation. This PR consolidates the copies that share the same contract.

While doing so, I found a memory bug in src/presign.rs. It built the header arrays by pushing into a Vec but freed them with Vec::from_raw_parts(ptr, len, len). A pushed Vec usually has more capacity than length, so the arrays were freed with the wrong layout, which is undefined behavior. It also leaked the strings already built when a header value was not visible ASCII. A temporary layout-checking allocator confirmed both problems before the fix and neither after it.

What changes are included in this PR?

Each commit is one self-contained change:

  • fix: Build and free presigned request headers with utils::string_pairs and release_string_pairs, which store exact-length boxed slices, and validate every header before allocating anything. The FFI struct layout is unchanged.
  • Release every owned C string (error messages, operator info fields, and entry paths) through utils::release_c_string.
  • Merge validate_non_zero_u64, validate_non_zero_u32, and validate_non_zero_usize into a generic validate_non_zero, and parse_usize and parse_u64 into parse_number. Stat now builds its metadata payload through into_metadata_ptr, like write and copy.
  • Forward the Task-returning SubmitAsyncOperation overloads to the Task<bool> overloads. This adds no delegate or closure allocation per call.
  • Share the bodies of the *_option_build and *_option_free pairs through generic build_options and free_options helpers. Every exported function keeps its own name, signature, and safety contract, and each build function still names the parser it uses.
  • Add require_input_stream and require_output_stream, mirroring require_op_handle, instead of repeating the null check and cast in each stream function.
  • Remove TryTakeAsyncState, which only forwarded to AsyncStateRegistry.TryTake.

I tested the changes with dotnet test on the memory service and on MinIO.

Are there any user-facing changes?

None.

Breaking changes

AI Usage Statement

  • Harness: Claude Code
  • Model: Claude Opus 5.5
  • Effort: xhigh
  • Role: AI reviewed the .NET binding for duplicated code, found the presign layout bug during that review, reproduced it with the temporary layout-checking allocator described above (not committed), and implemented each commit with local validation, including the MinIO presign checks. I reviewed every change and chose which consolidations to keep; a proposal to merge the per-operation async callbacks was dropped because it made call sites harder to read.
  • Unknowns: No regression test is committed for the presign fix, because detecting it needs a layout-checking allocator or Miri, and CI does not run this crate's Rust unit tests.

…yout

into_presigned_request_ptr built the header key and value arrays by pushing
into a Vec and leaking it with mem::forget, while presigned_request_free
rebuilt them with Vec::from_raw_parts(ptr, len, len). A pushed Vec usually
has capacity greater than len (1-3 headers leave capacity 4), so the arrays
were deallocated with the wrong layout, which is undefined behavior.

Reuse utils::string_pairs and release_string_pairs, which store the arrays
as exact-length boxed slices, and release_c_string for method and uri.
Headers are now validated before any string is allocated, so a header
value that is not visible ASCII no longer leaks the strings built so far.

The FFI struct layout is unchanged, so the C# side needs no update.
The error message, operator info fields, and entry paths each reimplemented
the null check and CString::from_raw that utils::release_c_string already
provides. Route them through the shared helper so every owned C string in
the FFI payloads is released the same way.
- Replace validate_non_zero_u64/u32/usize with one generic
  validate_positive_integer; the three bodies and error messages were
  identical.
- Replace parse_usize and parse_u64 with a generic parse_number; the error
  message still names the target type.
- Build stat metadata payloads through into_metadata_ptr like write and
  copy already do.
…l path

The Task-returning SubmitAsyncOperation overloads duplicated the Task<bool>
overloads line for line, since both register an AsyncState<bool>. Forward
them to the Task<bool> overloads instead. This adds no delegate or closure
allocation per call.
Each *_option_build and *_option_free pair repeated the same collect,
box, and release steps, differing only in the parser and payload type.
Move those steps into generic build_options and free_options helpers.

Every exported function keeps its own name, signature, and safety
contract, and each build function still names the parser it uses, so the
FFI surface is unchanged.
Every input and output stream function repeated the same null check and
unsafe cast of the opaque stream pointer. Add require_input_stream and
require_output_stream next to the stream types, mirroring
require_op_handle, so each function validates its handle in one line.

The null-pointer errors and their order relative to the other argument
checks are unchanged; the free functions keep treating null as a no-op.
TryTakeAsyncState only forwarded to AsyncStateRegistry.TryTake, which
already carries the NotNullWhen annotation. Call the registry directly,
as OnReadResultRetained already does, and drop the wrapper.
@github-actions github-actions Bot added bindings/dotnet releases-note/refactor The PR does a refactor on code or has a title that begins with "refactor" size:L This PR changes 100-499 lines, ignoring generated files. labels Sep 25, 2026

This branch has not been deployed

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

Labels

bindings/dotnet releases-note/refactor The PR does a refactor on code or has a title that begins with "refactor" size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant