Skip to content

Commit 84281b7

Browse files
authored
Various fixes (#39)
* Use proper C types for ccalls; this fixes UB and is more maintainable * Add fast path for append!(::Vector, ::MemoryView) * Add 5-arg copyto! to avoid generic fallback * Improve precision of @static VERSION checks * Simplify impl of old parentindices before memoryindex was public * Fix a couple of pointer retentions that was UB * Amend AGENTS.md
1 parent 18f4d0a commit 84281b7

File tree

6 files changed

+57
-30
lines changed

6 files changed

+57
-30
lines changed

AGENTS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ Set `--check-bounds=yes` to force boundschecking when running experiments.
2929
**Source files**:
3030
- `construction.jl` — constructors from Array, Memory, String, SubArray, CodeUnits
3131
- `basic.jl` — indexing, slicing (returns views, not copies), copying, find operations with memchr/memrchr C calls, comparison via memcmp
32-
- `experimental.jl``split_first`, `split_last`, `split_at`, `split_unaligned`
3332
- `delimited.jl``split_each` delimiter iterator
3433
- `base_arrays.jl` — Vector/Memory conversion, append
3534
- `io.jl``readbytes!`

examples/find.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function find_next_byte(needle::UInt8, haystack::ImmutableMemoryView{UInt8}, i::
7474
ulen = len % UInt
7575
GC.@preserve haystack begin
7676
ptr = pointer(haystack, i)
77-
p = @ccall memchr(ptr::Ptr{UInt8}, needle::UInt8, ulen::UInt)::Ptr{Nothing}
77+
p = @ccall memchr(ptr::Ptr{Cvoid}, needle::Cint, ulen::Csize_t)::Ptr{Cvoid}
7878
end
7979
return p == C_NULL ? nothing : (p - ptr + i) % Int
8080
end

src/MemoryViews.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ struct Immutable end
3131
"""
3232
MemoryView{T, M} <: DenseVector{T}
3333
34-
View into a `Memory{T}`.
34+
`MemoryView` is the common representation of dense, contiguous, Julia-owned
35+
data. `String`, `Vector`, `Memory` and other memory-backed types should have
36+
a fast `MemoryView` constructor.
37+
3538
Construct from memory-backed values `x` with `MemoryView(x)`.
3639
3740
`MemoryView`s are guaranteed to point to contiguous, valid CPU memory,
@@ -102,9 +105,11 @@ Create a mutable memory view from its parts.
102105
* All indices `i in 1:len` are valid for `ref` (i.e. `memoryref(ref, i)` would
103106
not throw)
104107
* If `ref` is derived from immutable memory, it is the caller's responsibility
105-
to ensure that mutating the memory does not result in undefined behaviour.
108+
to ensure that the resulting view is not mutated.
106109
For example, `ref` may be derived from a `String`, and mutating `String`s in
107110
Julia may result in undefined behaviour.
111+
In these cases, the caller may convert to `ImmutableMemoryView` immediately
112+
after calling this function.
108113
109114
# Examples
110115
```jldoctest

src/base_arrays.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function Base.copy!(A::Union{Memory{T}, Array{T}}, mem::MemoryView{T}) where {T}
2626
end
2727

2828
function Base.append!(v::Vector{T}, mem::MemoryView{T}) where {T}
29+
isempty(mem) && return v
2930
old_len = length(v)
3031
resize!(v, length(v) + length(mem))
3132
dst = @inbounds MemoryView(v)[(old_len + 1):end]

src/basic.jl

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ end
88

99
# The parent method for memoryref was added in 1.12. In versions before that,
1010
# it can be accessed by reaching into internals.
11-
@static if VERSION < v"1.12"
11+
@static if VERSION < v"1.12.0-DEV.966"
1212
Base.parent(v::MemoryView) = v.ref.mem
1313
else
1414
Base.parent(v::MemoryView) = parent(v.ref)
@@ -23,17 +23,10 @@ function Base.iterate(x::MemoryView, i::Int = 1)
2323
end
2424

2525
# Base.memoryindex exists in Julia 1.13 onwards.
26-
@static if VERSION < v"1.13"
26+
@static if VERSION < v"1.13.0-DEV.1289"
2727
function Base.parentindices(x::MemoryView)
28-
elz = Base.elsize(x)
29-
return if iszero(elz)
30-
offset = Int(x.ref.ptr_or_offset)
31-
((1 + offset):(x.len + offset),)
32-
else
33-
byte_offset = pointer(x.ref) - pointer(x.ref.mem)
34-
elem_offset = div(byte_offset % UInt, elz % UInt) % Int
35-
((elem_offset + 1):(elem_offset + x.len),)
36-
end
28+
start = Core.memoryrefoffset(x.ref)
29+
return (start:(start + length(x) - 1),)
3730
end
3831
else
3932
function Base.parentindices(x::MemoryView)
@@ -85,12 +78,14 @@ function Base.mightalias(a::MemoryView{T}, b::MemoryView{T}) where {T}
8578
(isempty(a) | isempty(b)) && return false
8679
# We can't compare the underlying Memory with === to add a fast path here,
8780
# because users can create aliasing, but distinct Memory using unsafe_wrap.
88-
(p1, p2) = (pointer(a), pointer(b))
89-
elz = Base.elsize(a)
90-
return if p1 < p2
91-
p1 + length(a) * elz > p2
92-
else
93-
p2 + length(b) * elz > p1
81+
GC.@preserve a b begin
82+
(p1, p2) = (pointer(a), pointer(b))
83+
elz = Base.elsize(a)
84+
return if p1 < p2
85+
p1 + length(a) * elz > p2
86+
else
87+
p2 + length(b) * elz > p1
88+
end
9489
end
9590
end
9691

@@ -183,12 +178,23 @@ function Base.copyto!(dst::MutableMemoryView{T}, src::MemoryView{T}) where {T}
183178
return unsafe_copyto!(dst, src)
184179
end
185180

181+
# This function is kind of bad API, and users should not use it. However, without this overload,
182+
# the fallback definition is used instead which is even worse.
183+
function Base.copyto!(dst::MutableMemoryView, di::Integer, src::MemoryView{T}, si::Integer, N::Integer) where {T}
184+
di = Int(di)::Int
185+
si = Int(si)::Int
186+
N = Int(N)::Int
187+
dst = dst[di:(di + N - 1)]
188+
src = src[si:(si + N - 1)]
189+
return copyto!(dst, src)
190+
end
191+
186192
function Base.fill!(v::MutableMemoryView{UInt8}, x::Integer)
187193
xT = convert(UInt8, x)::UInt8
188194
isempty(v) && return v
189195
GC.@preserve v @ccall memset(
190-
pointer(v)::Ptr{Nothing},
191-
Int32(xT)::Cint,
196+
pointer(v)::Ptr{Cvoid},
197+
Cint(xT)::Cint,
192198
(length(v) % UInt)::Csize_t
193199
)::Cvoid
194200
return v
@@ -252,7 +258,7 @@ function memchr(mem::ImmutableMemoryView{T}, byte::T) where {T <: Union{Int8, UI
252258
GC.@preserve mem begin
253259
ptr = Ptr{UInt8}(pointer(mem))
254260
p = @ccall memchr(
255-
ptr::Ptr{UInt8},
261+
ptr::Ptr{Cvoid},
256262
(byte % UInt8)::Cint,
257263
length(mem)::Csize_t,
258264
)::Ptr{Cvoid}
@@ -311,7 +317,7 @@ function memrchr(mem::ImmutableMemoryView{T}, byte::T) where {T <: Union{Int8, U
311317
GC.@preserve mem begin
312318
ptr = Ptr{UInt8}(pointer(mem))
313319
p = @ccall memrchr(
314-
ptr::Ptr{UInt8},
320+
ptr::Ptr{Cvoid},
315321
(byte % UInt8)::Cint,
316322
length(mem)::Csize_t,
317323
)::Ptr{Cvoid}
@@ -338,7 +344,7 @@ function Base.:(==)(a::Mem, b::Mem) where {Mem <: BitMemory}
338344
GC.@preserve a b begin
339345
aptr = Ptr{Nothing}(pointer(a))
340346
bptr = Ptr{Nothing}(pointer(b))
341-
y = @ccall memcmp(aptr::Ptr{Nothing}, bptr::Ptr{Nothing}, nbytes::Csize_t)::Cint
347+
y = @ccall memcmp(aptr::Ptr{Cvoid}, bptr::Ptr{Cvoid}, nbytes::Csize_t)::Cint
342348
end
343349
return iszero(y)
344350
end
@@ -349,9 +355,9 @@ function Base.cmp(a::MemoryView{UInt8}, b::MemoryView{UInt8})
349355
aptr = Ptr{Nothing}(pointer(a))
350356
bptr = Ptr{Nothing}(pointer(b))
351357
@ccall memcmp(
352-
aptr::Ptr{Nothing},
353-
bptr::Ptr{Nothing},
354-
min(length(a), length(b))::Int,
358+
aptr::Ptr{Cvoid},
359+
bptr::Ptr{Cvoid},
360+
min(length(a), length(b))::Csize_t,
355361
)::Cint
356362
end
357363
else
@@ -518,7 +524,8 @@ function split_unaligned(v::MemoryView{T, M}, ::Val{A}) where {A, T, M}
518524
# Early return here to avoid division by zero: Size sz is statically known,
519525
# this will be compiled away
520526
iszero(sz) && return (unsafe_new_memoryview(M, v.ref, 0), v)
521-
unaligned_bytes = ((alignment - (UInt(pointer(v)) & mask)) & mask)
527+
ptr_int = GC.@preserve v UInt(pointer(v))
528+
unaligned_bytes = ((alignment - (ptr_int & mask)) & mask)
522529
n_elements = min(length(v), div(unaligned_bytes, sz % UInt) % Int)
523530
return @inbounds split_at(v, n_elements + 1)
524531
end

test/runtests.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,21 @@ end
436436
v2 = rand(Int, 4)
437437
unsafe_copyto!(MemoryView(v1), MemoryView(v2))
438438
@test v2 == v1
439+
440+
# 5-argument copyto! should also stay on LightBoundsError rather than
441+
# falling back to Base's BoundsError-based array path.
442+
mem = MemoryView([1, 2, 3])
443+
@test_throws LightBoundsError copyto!(mem, UInt(1), mem, UInt(4), UInt(1))
444+
@test_throws LightBoundsError copyto!(mem, 4, mem, 1, 1)
445+
@test_throws LightBoundsError copyto!(mem, 1, mem, 1, 4)
446+
447+
mem = MemoryView([1, 2, 3, 4])
448+
@test copyto!(mem, 2, mem, 3, 2) == [3, 4]
449+
@test mem == [1, 3, 4, 4]
450+
451+
mem = MemoryView([1, 2, 3, 4])
452+
@test copyto!(mem, Int32(1), mem, Int32(3), Int32(2)) == [3, 4]
453+
@test mem == [3, 4, 3, 4]
439454
end
440455

441456
@testset "Reverse and reverse!" begin

0 commit comments

Comments
 (0)