This is an automated email from the ASF dual-hosted git repository.
Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 14a5d7b8af docs : add saftey comments to unsafe callsites (#10839)
14a5d7b8af is described below
commit 14a5d7b8af7084a39e8516c1feb59c026e6ecf77
Author: RIchard Baah <[email protected]>
AuthorDate: Wed Aug 26 21:54:56 2026 -0400
docs : add saftey comments to unsafe callsites (#10839)
# Which issue does this PR close?
<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax.
-->
- Closes #147
- Closes #149
- Closes #151
# Rationale for this change
see #147, #149, #151
<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->
# What changes are included in this PR?
adds doc comment to add unsafe call sites mentioned in the linked
issues.
**AI was used to help find and add doc comments**
<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->
# Are these changes tested?
n/a
<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code
If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
If this PR claims a performance improvement, please include evidence
such as benchmark results.
-->
# Are there any user-facing changes?
<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
If there are any breaking changes to public APIs, please call them out.
-->
---
arrow-array/src/ffi.rs | 10 +++++++---
arrow-buffer/src/util/bit_chunk_iterator.rs | 8 ++++++++
arrow-buffer/src/util/bit_util.rs | 5 +++++
arrow-schema/src/ffi.rs | 11 +++++++++++
4 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/arrow-array/src/ffi.rs b/arrow-array/src/ffi.rs
index d09c077c65..1957031c22 100644
--- a/arrow-array/src/ffi.rs
+++ b/arrow-array/src/ffi.rs
@@ -322,7 +322,9 @@ impl ImportedArrowArray<'_> {
child_data.push(d.consume()?);
}
- // Should FFI be checking validity?
+ // Safety: all fields (length, null_count, null buffer, data buffers,
child data) were
+ // derived from the C Data Interface schema and array, which the
caller of `from_ffi`
+ // guarantees follow the spec; the constructed `ArrayData` satisfies
its invariants.
Ok(unsafe {
ArrayData::new_unchecked(
self.data_type,
@@ -482,7 +484,9 @@ impl ImportedArrowArray<'_> {
// we assume that pointer is aligned for `i32`, as Utf8 uses
`i32` offsets.
#[expect(clippy::cast_ptr_alignment)]
let offset_buffer = self.array.buffer(1).cast::<i32>();
- // get last offset
+ // Safety: `len` is the byte length of the offset buffer;
dividing by `size_of::<i32>()`
+ // gives the number of i32 elements. The `- 1` is safe because
the array is non-empty
+ // (checked above), so the offset buffer has at least one
element.
(unsafe { *offset_buffer.add(len / size_of::<i32>() - 1) }) as
usize
}
(DataType::LargeUtf8 | DataType::LargeBinary, 2) => {
@@ -496,7 +500,7 @@ impl ImportedArrowArray<'_> {
// we assume that pointer is aligned for `i64`, as Large uses
`i64` offsets.
#[expect(clippy::cast_ptr_alignment)]
let offset_buffer = self.array.buffer(1).cast::<i64>();
- // get last offset
+ // Safety: same as the i32 case above but for i64 offsets.
(unsafe { *offset_buffer.add(len / size_of::<i64>() - 1) }) as
usize
}
// View types: these have variadic buffers.
diff --git a/arrow-buffer/src/util/bit_chunk_iterator.rs
b/arrow-buffer/src/util/bit_chunk_iterator.rs
index 460349e7a6..b67309962a 100644
--- a/arrow-buffer/src/util/bit_chunk_iterator.rs
+++ b/arrow-buffer/src/util/bit_chunk_iterator.rs
@@ -88,6 +88,7 @@ impl<'a> UnalignedBitChunk<'a> {
}
// Read into prefix and suffix as needed
+ // Safety: u64 has no invalid bit patterns so reinterpreting
initialized u8 bytes as u64 is sound.
let (prefix, mut chunks, suffix) = unsafe { buffer.align_to::<u64>() };
assert!(
prefix.len() < 8 && suffix.len() < 8,
@@ -284,6 +285,8 @@ impl<'a> BitChunks<'a> {
// might be one more than sizeof(u64) if the offset is in the
middle of a byte
let byte_len = ceil(bit_len + bit_offset, 8);
// pointer to remainder bytes after all complete chunks
+ // Safety: the buffer contains `chunk_len * 8 + ceil(remainder_len
+ bit_offset, 8)`
+ // bytes, so offsetting by `chunk_len * 8` and reading `byte_len`
bytes is in-bounds.
let base = unsafe {
self.buffer
.as_ptr()
@@ -373,6 +376,9 @@ impl Iterator for BitChunkIterator<'_> {
// bit-packed buffers are stored starting with the least-significant
byte first
// so when reading as u64 on a big-endian machine, the bytes need to
be swapped
+ // Safety: `index < self.chunk_len` and the buffer is at least
`chunk_len * 8` bytes long,
+ // so `raw_data.add(index)` is a valid in-bounds pointer;
`read_unaligned` handles
+ // any pointer alignment.
let current = unsafe {
std::ptr::read_unaligned(raw_data.add(index)).to_le() };
let bit_offset = self.bit_offset;
@@ -382,6 +388,8 @@ impl Iterator for BitChunkIterator<'_> {
} else {
// the constructor ensures that bit_offset is in 0..8
// that means we need to read at most one additional byte to fill
in the high bits
+ // Safety: the buffer has at least one byte past the last chunk
(the remainder byte
+ // needed for `bit_offset > 0`), so `index + 1` is within bounds.
let next =
unsafe { std::ptr::read_unaligned(raw_data.add(index +
1).cast::<u8>()) as u64 };
diff --git a/arrow-buffer/src/util/bit_util.rs
b/arrow-buffer/src/util/bit_util.rs
index d5da713e8a..f06e03ef13 100644
--- a/arrow-buffer/src/util/bit_util.rs
+++ b/arrow-buffer/src/util/bit_util.rs
@@ -664,6 +664,8 @@ impl<'a> U64UnalignedSlice<'a> {
// make the last pointer invalid, we handle the first element outside
the loop
// and then advance the pointer at the start of the loop
// making sure that the iterator is not empty
+ // Safety: `self.len > 0` (checked above) and the pointer has not been
advanced yet,
+ // so it is valid for reads and writes.
unsafe {
// I hope the function get inlined and the compiler remove the
dead right parameter
self.apply_bin_op(0, &mut |left, _| map(left));
@@ -794,6 +796,9 @@ fn set_remainder_bits(start_remainder_mut_slice: &mut [u8],
rem: u64, remainder_
// without calling `to_byte_slice` for each element,
// which is correct for all ArrowNativeType implementations including
u64.
let src = rem.as_ptr();
+ // Safety: `rem` has length `remainder_bytes`,
`start_remainder_mut_slice` has length
+ // `remainder_bytes`, and the two slices are non-overlapping (rem is
derived from a
+ // local `to_le_bytes()` call; start_remainder_mut_slice is the
caller's mutable buffer).
unsafe {
std::ptr::copy_nonoverlapping(
src,
diff --git a/arrow-schema/src/ffi.rs b/arrow-schema/src/ffi.rs
index 867d1409b9..679ced42d5 100644
--- a/arrow-schema/src/ffi.rs
+++ b/arrow-schema/src/ffi.rs
@@ -260,6 +260,9 @@ impl FFI_ArrowSchema {
None
};
+ // Safety: `self.private_data` was allocated as
`Box<SchemaPrivateData>` by `try_new`.
+ // We take ownership temporarily with `from_raw` and put it back with
`into_raw`,
+ // so there is no double-free and no other code can access
`private_data` concurrently.
unsafe {
let mut private_data =
Box::from_raw(self.private_data.cast::<SchemaPrivateData>());
private_data.metadata = new_metadata;
@@ -403,6 +406,8 @@ impl FFI_ArrowSchema {
///
/// This must be `Some` if the schema represents a dictionary-encoded
type, `None` otherwise.
pub fn dictionary(&self) -> Option<&Self> {
+ // Safety: per the C Data Interface spec, `self.dictionary` is either
null (returns None)
+ // or a valid pointer to an `FFI_ArrowSchema` that lives at least as
long as `self`.
unsafe { self.dictionary.as_ref() }
}
@@ -429,6 +434,8 @@ impl FFI_ArrowSchema {
let buffer = self.metadata.cast::<u8>();
fn next_four_bytes(buffer: *const u8, pos: &mut isize) -> [u8; 4] {
+ // Safety: the caller advances `pos` only by the number of
bytes consumed,
+ // so `*pos..*pos+4` is always within the bounds of the
metadata buffer.
let out = unsafe {
[
*buffer.offset(*pos),
@@ -442,6 +449,7 @@ impl FFI_ArrowSchema {
}
fn next_n_bytes(buffer: *const u8, pos: &mut isize, n: i32) ->
&[u8] {
+ // Safety: same as `next_four_bytes`; `*pos..*pos+n` is within
the metadata buffer.
let out = unsafe {
std::slice::from_raw_parts(buffer.offset(*pos),
n.try_into().unwrap())
};
@@ -487,6 +495,9 @@ impl Drop for FFI_ArrowSchema {
fn drop(&mut self) {
match self.release {
None => (),
+ // Safety: the release callback was set by the schema producer and
follows the
+ // C Data Interface contract: it frees all resources associated
with the schema
+ // and sets `release` to None. `self` is a valid, non-null pointer
here.
Some(release) => unsafe { release(self) },
}
}