diff --git a/pkg/gohacks/BUILD b/pkg/gohacks/BUILD index 38b4db9a6..4d88bc1bb 100644 --- a/pkg/gohacks/BUILD +++ b/pkg/gohacks/BUILD @@ -8,6 +8,8 @@ go_library( "gohacks_unsafe.go", "slice_go113_unsafe.go", "slice_go120_unsafe.go", + "string_go113_unsafe.go", + "string_go120_unsafe.go", ], stateify = False, visibility = ["//:sandbox"], @@ -16,7 +18,10 @@ go_library( go_test( name = "gohacks_test", size = "small", - srcs = ["gohacks_test.go"], + srcs = [ + "gohacks_test.go", + "string_test.go", + ], library = ":gohacks", deps = ["@org_golang_x_sys//unix:go_default_library"], ) diff --git a/pkg/gohacks/gohacks_unsafe.go b/pkg/gohacks/gohacks_unsafe.go index 6452163a4..c4186763c 100644 --- a/pkg/gohacks/gohacks_unsafe.go +++ b/pkg/gohacks/gohacks_unsafe.go @@ -30,14 +30,6 @@ import ( "unsafe" ) -// StringHeader is equivalent to reflect.StringHeader, but represents the -// pointer to the underlying array as unsafe.Pointer rather than uintptr, -// allowing StringHeaders to be directly converted to strings. -type StringHeader struct { - Data unsafe.Pointer - Len int -} - // Noescape hides a pointer from escape analysis. Noescape is the identity // function but escape analysis doesn't think the output depends on the input. // Noescape is inlined and currently compiles down to zero instructions. @@ -51,24 +43,6 @@ func Noescape(p unsafe.Pointer) unsafe.Pointer { return unsafe.Pointer(x ^ 0) } -// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the -// same memory backing s instead of making a heap-allocated copy. This is only -// valid if the returned slice is never mutated. -func ImmutableBytesFromString(s string) []byte { - shdr := (*StringHeader)(unsafe.Pointer(&s)) - return Slice((*byte)(shdr.Data), shdr.Len) -} - -// StringFromImmutableBytes is equivalent to string(bs), except that it uses -// the same memory backing bs instead of making a heap-allocated copy. This is -// only valid if bs is never mutated after StringFromImmutableBytes returns. -func StringFromImmutableBytes(bs []byte) string { - // This is cheaper than messing with StringHeader and SliceHeader, which as - // of this writing produces many dead stores of zeroes. Compare - // strings.Builder.String(). - return *(*string)(unsafe.Pointer(&bs)) -} - // Note that go:linkname silently doesn't work if the local name is exported, // necessitating an indirection for exported functions. diff --git a/pkg/gohacks/string_go113_unsafe.go b/pkg/gohacks/string_go113_unsafe.go new file mode 100644 index 000000000..dceeaf576 --- /dev/null +++ b/pkg/gohacks/string_go113_unsafe.go @@ -0,0 +1,51 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.13 && !go1.20 +// +build go1.13,!go1.20 + +// TODO(go.dev/issue/8422): Remove this file once Go 1.19 is no longer +// supported. + +package gohacks + +import ( + "unsafe" +) + +// stringHeader is equivalent to reflect.StringHeader, but represents the +// pointer to the underlying array as unsafe.Pointer rather than uintptr, +// allowing StringHeaders to be directly converted to strings. +type stringHeader struct { + Data unsafe.Pointer + Len int +} + +// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the +// same memory backing s instead of making a heap-allocated copy. This is only +// valid if the returned slice is never mutated. +func ImmutableBytesFromString(s string) []byte { + shdr := (*stringHeader)(unsafe.Pointer(&s)) + return Slice((*byte)(shdr.Data), shdr.Len) +} + +// StringFromImmutableBytes is equivalent to string(bs), except that it uses +// the same memory backing bs instead of making a heap-allocated copy. This is +// only valid if bs is never mutated after StringFromImmutableBytes returns. +func StringFromImmutableBytes(bs []byte) string { + // This is cheaper than messing with StringHeader and SliceHeader, which as + // of this writing produces many dead stores of zeroes. Compare + // strings.Builder.String(). + return *(*string)(unsafe.Pointer(&bs)) +} diff --git a/pkg/gohacks/string_go120_unsafe.go b/pkg/gohacks/string_go120_unsafe.go new file mode 100644 index 000000000..9005efd6a --- /dev/null +++ b/pkg/gohacks/string_go120_unsafe.go @@ -0,0 +1,39 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.20 + +package gohacks + +import ( + "unsafe" +) + +// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the +// same memory backing s instead of making a heap-allocated copy. This is only +// valid if the returned slice is never mutated. +func ImmutableBytesFromString(s string) []byte { + b := unsafe.StringData(s) + return unsafe.Slice(b, len(s)) +} + +// StringFromImmutableBytes is equivalent to string(bs), except that it uses +// the same memory backing bs instead of making a heap-allocated copy. This is +// only valid if bs is never mutated after StringFromImmutableBytes returns. +func StringFromImmutableBytes(bs []byte) string { + if len(bs) == 0 { + return "" + } + return unsafe.String(&bs[0], len(bs)) +} diff --git a/pkg/gohacks/string_test.go b/pkg/gohacks/string_test.go new file mode 100644 index 000000000..8c1c79346 --- /dev/null +++ b/pkg/gohacks/string_test.go @@ -0,0 +1,86 @@ +// Copyright 2023 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gohacks + +import ( + "reflect" + "testing" +) + +func TestImmutableBytesFromString(t *testing.T) { + tests := []struct { + name string + input string + want []byte + }{ + { + name: "abc", + input: "abc", + want: []byte("abc"), + }, + { + name: "empty", + input: "", + want: nil, + }, + { + name: "subslice-empty", + input: "abc"[:0], + want: []byte(""), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := ImmutableBytesFromString(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("got contents %v (len %d cap %d) want %v (len %d cap %d)", got, len(got), cap(got), tc.want, len(tc.want), cap(tc.want)) + } + }) + } +} + +func TestStringFromImmutableBytes(t *testing.T) { + tests := []struct { + name string + input []byte + want string + }{ + { + name: "abc", + input: []byte("abc"), + want: "abc", + }, + { + name: "empty", + input: []byte{}, + want: "", + }, + { + name: "nil", + input: nil, + want: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := StringFromImmutableBytes(tc.input) + if got != tc.want { + t.Errorf("got %q want %q", got, tc.want) + } + }) + } +}