Skip to content

Add takestring!(x) to create a string from the content of x, emptying it #54372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Compiler/src/ssair/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ function compute_ir_line_annotations(code::IRCode)
loc_method = string(" "^printing_depth, loc_method)
last_stack = stack
end
push!(loc_annotations, String(take!(buf)))
push!(loc_annotations, takestring!(buf))
push!(loc_lineno, (lineno != 0 && lineno != last_lineno) ? string(lineno) : "")
push!(loc_methods, loc_method)
(lineno != 0) && (last_lineno = lineno)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ New library features

* `sort(keys(::Dict))` and `sort(values(::Dict))` now automatically collect, they previously threw ([#56978]).
* `Base.AbstractOneTo` is added as a supertype of one-based axes, with `Base.OneTo` as its subtype ([#56902]).
* `takestring!(::IOBuffer)` removes the content from the buffer, returning the content as a `String`.

Standard library changes
------------------------
Expand Down
4 changes: 2 additions & 2 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ function showerror(io::IO, ex::MethodError)
iob = IOContext(buf, io) # for type abbreviation as in #49795; some, like `convert(T, x)`, should not abbreviate
show_signature_function(iob, Core.Typeof(f))
show_tuple_as_call(iob, :function, arg_types; hasfirst=false, kwargs = isempty(kwargs) ? nothing : kwargs)
str = String(take!(buf))
str = takestring!(buf)
str = type_limited_string_from_context(io, str)
print(io, str)
end
Expand Down Expand Up @@ -600,7 +600,7 @@ function show_method_candidates(io::IO, ex::MethodError, kwargs=[])
m = parentmodule_before_main(method)
modulecolor = get!(() -> popfirst!(STACKTRACE_MODULECOLORS), STACKTRACE_FIXEDCOLORS, m)
print_module_path_file(iob, m, string(file), line; modulecolor, digit_align_width = 3)
push!(lines, String(take!(buf)))
push!(lines, takestring!(buf))
push!(line_score, -(right_matches * 2 + (length(arg_types_param) < 2 ? 1 : 0)))
end
end
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ export
split,
string,
strip,
takestring!,
textwidth,
thisind,
titlecase,
Expand Down
2 changes: 1 addition & 1 deletion base/filesystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ import .Base:
bytesavailable, position, read, read!, readbytes!, readavailable, seek, seekend, show,
skip, stat, unsafe_read, unsafe_write, write, transcode, uv_error, _uv_error,
setup_stdio, rawhandle, OS_HANDLE, INVALID_OS_HANDLE, windowserror, filesize,
isexecutable, isreadable, iswritable, MutableDenseArrayType, truncate
isexecutable, isreadable, iswritable, MutableDenseArrayType, truncate, unsafe_takestring!

import .Base.RefValue

Expand Down
2 changes: 1 addition & 1 deletion base/indices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ function throw_promote_shape_mismatch(a::Tuple, b::Union{Nothing,Tuple}, i = not
if i ≢ nothing
print(msg, ", mismatch at dim ", i)
end
throw(DimensionMismatch(String(take!(msg))))
throw(DimensionMismatch(takestring!(msg)))
end

function promote_shape(a::Tuple{Int,}, b::Tuple{Int,})
Expand Down
2 changes: 1 addition & 1 deletion base/int.jl
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ macro big_str(s::String)
is_prev_dot = (c == '.')
end
print(bf, s[end])
s = String(take!(bf))
s = unsafe_takestring!(bf)
end
n = tryparse(BigInt, s)
n === nothing || return n
Expand Down
21 changes: 10 additions & 11 deletions base/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,13 @@ julia> io = IOBuffer();
julia> write(io, "JuliaLang is a GitHub organization.", " It has many members.")
56

julia> String(take!(io))
julia> takestring!(io)
"JuliaLang is a GitHub organization. It has many members."

julia> write(io, "Sometimes those members") + write(io, " write documentation.")
44

julia> String(take!(io))
julia> takestring!(io)
"Sometimes those members write documentation."
```
User-defined plain-data types without `write` methods can be written when wrapped in a `Ref`:
Expand Down Expand Up @@ -544,7 +544,7 @@ julia> rm("my_file.txt")
"""
readuntil(filename::AbstractString, delim; kw...) = open(io->readuntil(io, delim; kw...), convert(String, filename)::String)
readuntil(stream::IO, delim::UInt8; kw...) = _unsafe_take!(copyuntil(IOBuffer(sizehint=16), stream, delim; kw...))
readuntil(stream::IO, delim::Union{AbstractChar, AbstractString}; kw...) = String(_unsafe_take!(copyuntil(IOBuffer(sizehint=16), stream, delim; kw...)))
readuntil(stream::IO, delim::Union{AbstractChar, AbstractString}; kw...) = takestring!(copyuntil(IOBuffer(sizehint=16), stream, delim; kw...))
readuntil(stream::IO, delim::T; keep::Bool=false) where T = _copyuntil(Vector{T}(), stream, delim, keep)


Expand All @@ -566,10 +566,10 @@ Similar to [`readuntil`](@ref), which returns a `String`; in contrast,
```jldoctest
julia> write("my_file.txt", "JuliaLang is a GitHub organization.\\nIt has many members.\\n");

julia> String(take!(copyuntil(IOBuffer(), "my_file.txt", 'L')))
julia> takestring!(copyuntil(IOBuffer(), "my_file.txt", 'L'))
"Julia"

julia> String(take!(copyuntil(IOBuffer(), "my_file.txt", '.', keep = true)))
julia> takestring!(copyuntil(IOBuffer(), "my_file.txt", '.', keep = true))
"JuliaLang is a GitHub organization."

julia> rm("my_file.txt")
Expand Down Expand Up @@ -616,8 +616,7 @@ Logan
"""
readline(filename::AbstractString; keep::Bool=false) =
open(io -> readline(io; keep), filename)
readline(s::IO=stdin; keep::Bool=false) =
String(_unsafe_take!(copyline(IOBuffer(sizehint=16), s; keep)))
readline(s::IO=stdin; keep::Bool=false) = takestring!(copyline(IOBuffer(sizehint=16), s; keep))

"""
copyline(out::IO, io::IO=stdin; keep::Bool=false)
Expand All @@ -642,10 +641,10 @@ See also [`copyuntil`](@ref) for reading until more general delimiters.
```jldoctest
julia> write("my_file.txt", "JuliaLang is a GitHub organization.\\nIt has many members.\\n");

julia> String(take!(copyline(IOBuffer(), "my_file.txt")))
julia> takestring!(copyline(IOBuffer(), "my_file.txt"))
"JuliaLang is a GitHub organization."

julia> String(take!(copyline(IOBuffer(), "my_file.txt", keep=true)))
julia> takestring!(copyline(IOBuffer(), "my_file.txt", keep=true))
"JuliaLang is a GitHub organization.\\n"

julia> rm("my_file.txt")
Expand Down Expand Up @@ -1290,7 +1289,7 @@ function iterate(r::Iterators.Reverse{<:EachLine}, state)
buf.size = _stripnewline(r.itr.keep, buf.size, buf.data)
empty!(chunks) # will cause next iteration to terminate
seekend(r.itr.stream) # reposition to end of stream for isdone
s = String(_unsafe_take!(buf))
s = unsafe_takestring!(buf)
else
# extract the string from chunks[ichunk][inewline+1] to chunks[jchunk][jnewline]
if ichunk == jchunk # common case: current and previous newline in same chunk
Expand All @@ -1307,7 +1306,7 @@ function iterate(r::Iterators.Reverse{<:EachLine}, state)
end
write(buf, view(chunks[jchunk], 1:jnewline))
buf.size = _stripnewline(r.itr.keep, buf.size, buf.data)
s = String(_unsafe_take!(buf))
s = unsafe_takestring!(buf)

# overwrite obsolete chunks (ichunk+1:jchunk)
i = jchunk
Expand Down
78 changes: 76 additions & 2 deletions base/iobuffer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ julia> io = IOBuffer();
julia> write(io, "JuliaLang is a GitHub organization.", " It has many members.")
56

julia> String(take!(io))
julia> takestring!(io)
"JuliaLang is a GitHub organization. It has many members."

julia> io = IOBuffer(b"JuliaLang is a GitHub organization.")
Expand All @@ -216,7 +216,7 @@ IOBuffer(data=UInt8[...], readable=true, writable=true, seekable=true, append=fa
julia> write(io, "JuliaLang is a GitHub organization.")
34

julia> String(take!(io))
julia> takestring!(io)
"JuliaLang is a GitHub organization"

julia> length(read(IOBuffer(b"data", read=true, truncate=false)))
Expand Down Expand Up @@ -783,6 +783,80 @@ function take!(io::IOBuffer)
return data
end

"Internal method. This method can be faster than takestring!, because it does not
reset the buffer to a usable state, and it does not check for io.reinit.
Using the buffer after calling unsafe_takestring! may cause undefined behaviour.
This function is meant to be used when the buffer is only used as a temporary
string builder, which is discarded after the string is built."
function unsafe_takestring!(io::IOBuffer)
used_span = get_used_span(io)
nbytes = length(used_span)
from = first(used_span)
isempty(used_span) && return ""
# The C function can only copy from the start of the memory.
# Fortunately, in most cases, the offset will be zero.
return if isone(from)
ccall(:jl_genericmemory_to_string, Ref{String}, (Any, Int), io.data, nbytes)
else
mem = StringMemory(nbytes % UInt)
unsafe_copyto!(mem, 1, io.data, from, nbytes)
unsafe_takestring(mem)
end
end

"""
takestring!(io::IOBuffer) -> String

Return the content of `io` as a `String`, resetting the buffer to its initial
state.
This is preferred over calling `String(take!(io))` to create a string from
an `IOBuffer`.

# Examples
```jldoctest
julia> io = IOBuffer();

julia> write(io, [0x61, 0x62, 0x63]);

julia> s = takestring!(io)
"abc"

julia> isempty(take!(io)) # io is now empty
true
```

!!! compat "Julia 1.13"
This function requires at least Julia 1.13.
"""
function takestring!(io::IOBuffer)
# If the buffer has been used up and needs to be replaced, there are no bytes, and
# we can return an empty string without interacting with the buffer at all.
io.reinit && return ""

# If the iobuffer is writable, taking will remove the buffer from `io`.
# So, we reset the iobuffer, and directly unsafe takestring.
return if io.writable
s = unsafe_takestring!(io)
io.reinit = true
io.mark = -1
io.ptr = 1
io.size = 0
io.offset_or_compacted = 0
s
else
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is jl_genericmemory_to_string (the no-copy path) only reachable if the io is writable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, takestring! is mainly used for IOBuffers that are used as string builders, so they are already writable.
Also, the last time I checked, calling jl_genericmemory_to_string can make the reading from the memory invalid: #54372 (comment) , though this might have changed since then.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When IOBuffer is not writable, the IOBuffer doesn't take ownership of the memory, and allows it to be shared, e.g. between multiple IOBuffers (and that also makes IOBuffer more safe in general since the user can keep a reference to the memory they wrapped the IOBuffer around). Therefore, we need to copy it out.
Luckily, as Nathan said, that's usually not a performance problem for takestring! where the iobuffer is mutable.

# If the buffer is not writable, taking will NOT remove the buffer,
# so if we just converted the buffer to a string, garbage collecting
# the string would free the memory underneath the iobuffer
used_span = get_used_span(io)
mem = StringMemory(length(used_span))
unsafe_copyto!(mem, 1, io.data, first(used_span), length(used_span))
unsafe_takestring(mem)
end
end

# Fallback methods
takestring!(io::GenericIOBuffer) = String(take!(io))

"""
_unsafe_take!(io::IOBuffer)

Expand Down
6 changes: 3 additions & 3 deletions base/iostream.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ julia> write(io, "JuliaLang is a GitHub organization.")
julia> truncate(io, 15)
IOBuffer(data=UInt8[...], readable=true, writable=true, seekable=true, append=false, size=15, maxsize=Inf, ptr=16, mark=-1)

julia> String(take!(io))
julia> takestring!(io)
"JuliaLang is a "

julia> io = IOBuffer();
Expand All @@ -120,7 +120,7 @@ julia> write(io, "JuliaLang is a GitHub organization.");

julia> truncate(io, 40);

julia> String(take!(io))
julia> takestring!(io)
"JuliaLang is a GitHub organization.\\0\\0\\0\\0\\0"
```
"""
Expand Down Expand Up @@ -469,7 +469,7 @@ function readuntil_string(s::IOStream, delim::UInt8, keep::Bool)
end
readuntil(s::IOStream, delim::AbstractChar; keep::Bool=false) =
isascii(delim) ? readuntil_string(s, delim % UInt8, keep) :
String(_unsafe_take!(copyuntil(IOBuffer(sizehint=70), s, delim; keep)))
takestring!(copyuntil(IOBuffer(sizehint=70), s, delim; keep))

function readline(s::IOStream; keep::Bool=false)
@_lock_ios s ccall(:jl_readuntil, Ref{String}, (Ptr{Cvoid}, UInt8, UInt8, UInt8), s.ios, '\n', 1, keep ? 0 : 2)
Expand Down
2 changes: 1 addition & 1 deletion base/logging/ConsoleLogger.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function handle_message(logger::ConsoleLogger, level::LogLevel, message, _module
for (key, val) in kwargs
key === :maxlog && continue
showvalue(valio, val)
vallines = split(String(take!(valbuf)), '\n')
vallines = split(takestring!(valbuf), '\n')
if length(vallines) == 1
push!(msglines, (indent=2, msg=SubString("$key = $(vallines[1])")))
else
Expand Down
2 changes: 1 addition & 1 deletion base/pkgid.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function binpack(pkg::PkgId)
uuid = pkg.uuid
write(io, uuid === nothing ? UInt128(0) : UInt128(uuid))
write(io, pkg.name)
return String(take!(io))
return unsafe_takestring!(io)
end

function binunpack(s::String)
Expand Down
10 changes: 5 additions & 5 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,12 @@ julia> io = IOBuffer();

julia> printstyled(IOContext(io, :color => true), "string", color=:red)

julia> String(take!(io))
julia> takestring!(io)
"\\e[31mstring\\e[39m"

julia> printstyled(io, "string", color=:red)

julia> String(take!(io))
julia> takestring!(io)
"string"
```

Expand Down Expand Up @@ -2649,7 +2649,7 @@ function show_tuple_as_call(out::IO, name::Symbol, sig::Type;
end
print_within_stacktrace(io, ")", bold=true)
show_method_params(io, tv)
str = String(take!(buf))
str = takestring!(buf)
str = type_limited_string_from_context(out, str)
print(out, str)
nothing
Expand Down Expand Up @@ -2758,7 +2758,7 @@ function type_depth_limit(str::String, n::Int; maxdepth = nothing)
end
prev = di
end
return String(take!(output))
return unsafe_takestring!(output)
end

function print_type_bicolor(io, type; kwargs...)
Expand Down Expand Up @@ -3193,7 +3193,7 @@ summary(io::IO, x) = print(io, typeof(x))
function summary(x)
io = IOBuffer()
summary(io, x)
String(take!(io))
takestring!(io)
end

## `summary` for AbstractArrays
Expand Down
2 changes: 1 addition & 1 deletion base/stat.jl
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function filemode_string(mode)
end
complete && write(str, "-")
end
return String(take!(str))
return unsafe_takestring!(str)
end

"""
Expand Down
4 changes: 2 additions & 2 deletions base/strings/annotated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ function annotatedstring(xs...)
print(s, x)
end
end
str = String(take!(buf))
str = takestring!(buf)
AnnotatedString(str, annotations)
end

Expand Down Expand Up @@ -457,7 +457,7 @@ function annotated_chartransform(f::Function, str::AnnotatedString, state=nothin
stop_offset = last(offsets[findlast(<=(stop) ∘ first, offsets)::Int])
push!(annots, setindex(annot, (start + start_offset):(stop + stop_offset), :region))
end
AnnotatedString(String(take!(outstr)), annots)
AnnotatedString(takestring!(outstr), annots)
end

struct RegionIterator{S <: AbstractString}
Expand Down
2 changes: 1 addition & 1 deletion base/strings/basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ function filter(f, s::AbstractString)
for c in s
f(c) && write(out, c)
end
String(_unsafe_take!(out))
takestring!(out)
end

## string first and last ##
Expand Down
Loading