Repository: segmentio/parquet-go
Pull request #222 (2022-06-08 18:10:14)

use internal/unsafecast for all type conversions
First step towards removing the `internal/bits` package, this PR moves all type conversions to the `internal/unsafecast` package.

I also reduced use of the `unsafe` package across the code base, moving to `internal/unsafecast` in places where it made more sense and increased maintainability to use the abstractions.


Patch:
diff --git a/array.go b/array.go
index d3e8434..2038e2b 100644
--- a/array.go
+++ b/array.go
@@ -1,12 +1,22 @@
 package parquet
 
-import "unsafe"
+import (
+	"unsafe"
+)
 
 type array struct {
 	ptr unsafe.Pointer
 	len int
 }
 
+func makeArray(ptr unsafe.Pointer, len int) array {
+	return array{ptr: ptr, len: len}
+}
+
+func makeArrayBool(values []bool) array {
+	return *(*array)(unsafe.Pointer(&values))
+}
+
 func makeArrayString(values []string) array {
 	return *(*array)(unsafe.Pointer(&values))
 }
diff --git a/array_go18.go b/array_go18.go
index 841f3ed..c912946 100644
--- a/array_go18.go
+++ b/array_go18.go
@@ -2,13 +2,14 @@
 
 package parquet
 
-import "unsafe"
+import (
+	"unsafe"
 
-func makeArray[T any](s []T) array {
-	return array{
-		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&s)),
-		len: len(s),
-	}
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+)
+
+func makeArrayOf[T any](s []T) array {
+	return makeArray(unsafecast.PointerOf(s), len(s))
 }
 
 func makeSlice[T any](a array) []T {
diff --git a/bloom.go b/bloom.go
index 2683498..b2ea7ec 100644
--- a/bloom.go
+++ b/bloom.go
@@ -8,7 +8,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/format"
-	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 // BloomFilter is an interface allowing applications to test whether a key
@@ -149,12 +149,12 @@ func (splitBlockEncoding) EncodeBoolean(dst, src []byte) ([]byte, error) {
 }
 
 func (splitBlockEncoding) EncodeInt32(dst, src []byte) ([]byte, error) {
-	splitBlockEncodeUint32(bloom.MakeSplitBlockFilter(dst), bits.BytesToUint32(src))
+	splitBlockEncodeUint32(bloom.MakeSplitBlockFilter(dst), unsafecast.BytesToUint32(src))
 	return dst, nil
 }
 
 func (splitBlockEncoding) EncodeInt64(dst, src []byte) ([]byte, error) {
-	splitBlockEncodeUint64(bloom.MakeSplitBlockFilter(dst), bits.BytesToUint64(src))
+	splitBlockEncodeUint64(bloom.MakeSplitBlockFilter(dst), unsafecast.BytesToUint64(src))
 	return dst, nil
 }
 
@@ -164,12 +164,12 @@ func (e splitBlockEncoding) EncodeInt96(dst, src []byte) ([]byte, error) {
 }
 
 func (splitBlockEncoding) EncodeFloat(dst, src []byte) ([]byte, error) {
-	splitBlockEncodeUint32(bloom.MakeSplitBlockFilter(dst), bits.BytesToUint32(src))
+	splitBlockEncodeUint32(bloom.MakeSplitBlockFilter(dst), unsafecast.BytesToUint32(src))
 	return dst, nil
 }
 
 func (splitBlockEncoding) EncodeDouble(dst, src []byte) ([]byte, error) {
-	splitBlockEncodeUint64(bloom.MakeSplitBlockFilter(dst), bits.BytesToUint64(src))
+	splitBlockEncodeUint64(bloom.MakeSplitBlockFilter(dst), unsafecast.BytesToUint64(src))
 	return dst, nil
 }
 
@@ -193,7 +193,7 @@ func (splitBlockEncoding) EncodeByteArray(dst, src []byte) ([]byte, error) {
 func (splitBlockEncoding) EncodeFixedLenByteArray(dst, src []byte, size int) ([]byte, error) {
 	filter := bloom.MakeSplitBlockFilter(dst)
 	if size == 16 {
-		splitBlockEncodeUint128(filter, bits.BytesToUint128(src))
+		splitBlockEncodeUint128(filter, unsafecast.BytesToUint128(src))
 	} else {
 		splitBlockEncodeFixedLenByteArray(filter, src, size)
 	}
diff --git a/buffer_go18.go b/buffer_go18.go
index 5eb2433..3a28a08 100644
--- a/buffer_go18.go
+++ b/buffer_go18.go
@@ -72,7 +72,7 @@ func makeBufferFunc[T any](t reflect.Type, schema *Schema) bufferFunc[T] {
 	size := t.Size()
 	writeRows := writeRowsFuncOf(t, schema, nil)
 	return func(buf *GenericBuffer[T], rows []T) (n int, err error) {
-		err = writeRows(buf.base.columns, makeArray(rows), size, 0, columnLevels{})
+		err = writeRows(buf.base.columns, makeArrayOf(rows), size, 0, columnLevels{})
 		if err == nil {
 			n = len(rows)
 		}
diff --git a/column_buffer.go b/column_buffer.go
index 554541f..db5d062 100644
--- a/column_buffer.go
+++ b/column_buffer.go
@@ -10,6 +10,7 @@ import (
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 // ColumnBuffer is an interface representing columns of a row group.
@@ -805,11 +806,7 @@ func (col *booleanColumnBuffer) Swap(i, j int) {
 }
 
 func (col *booleanColumnBuffer) WriteBooleans(values []bool) (int, error) {
-	var rows = array{
-		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&values)),
-		len: len(values),
-	}
-	col.writeValues(rows, unsafe.Sizeof(false), 0, columnLevels{})
+	col.writeValues(makeArrayBool(values), unsafe.Sizeof(false), 0, columnLevels{})
 	return len(values), nil
 }
 
@@ -967,7 +964,7 @@ func (col *int32ColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 4) != 0 {
 		return 0, fmt.Errorf("cannot write INT32 values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToInt32(b)...)
+	col.values = append(col.values, unsafecast.BytesToInt32(b)...)
 	return len(b), nil
 }
 
@@ -1069,7 +1066,7 @@ func (col *int64ColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 8) != 0 {
 		return 0, fmt.Errorf("cannot write INT64 values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToInt64(b)...)
+	col.values = append(col.values, unsafecast.BytesToInt64(b)...)
 	return len(b), nil
 }
 
@@ -1263,7 +1260,7 @@ func (col *floatColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 4) != 0 {
 		return 0, fmt.Errorf("cannot write FLOAT values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToFloat32(b)...)
+	col.values = append(col.values, unsafecast.BytesToFloat32(b)...)
 	return len(b), nil
 }
 
@@ -1364,7 +1361,7 @@ func (col *doubleColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 8) != 0 {
 		return 0, fmt.Errorf("cannot write DOUBLE values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToFloat64(b)...)
+	col.values = append(col.values, unsafecast.BytesToFloat64(b)...)
 	return len(b), nil
 }
 
@@ -1518,7 +1515,7 @@ func (col *byteArrayColumnBuffer) writeByteArrays(values []byte) (count, bytes i
 	baseCount, baseBytes := len(col.offsets), len(col.values)
 
 	err = plain.RangeByteArrays(values, func(value []byte) error {
-		col.append(bits.BytesToString(value))
+		col.append(unsafecast.BytesToString(value))
 		return nil
 	})
 
@@ -1742,7 +1739,7 @@ func (col *uint32ColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 4) != 0 {
 		return 0, fmt.Errorf("cannot write INT32 values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToUint32(b)...)
+	col.values = append(col.values, unsafecast.BytesToUint32(b)...)
 	return len(b), nil
 }
 
@@ -1843,7 +1840,7 @@ func (col *uint64ColumnBuffer) Write(b []byte) (int, error) {
 	if (len(b) % 8) != 0 {
 		return 0, fmt.Errorf("cannot write INT64 values from input of size %d", len(b))
 	}
-	col.values = append(col.values, bits.BytesToUint64(b)...)
+	col.values = append(col.values, unsafecast.BytesToUint64(b)...)
 	return len(b), nil
 }
 
diff --git a/column_buffer_amd64.go b/column_buffer_amd64.go
index 64d2e22..0bd7879 100644
--- a/column_buffer_amd64.go
+++ b/column_buffer_amd64.go
@@ -5,11 +5,12 @@ package parquet
 import (
 	"unsafe"
 
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 	"golang.org/x/sys/cpu"
 )
 
 func broadcastValueInt32(dst []int32, src int8) {
-	memset(unsafe.Slice(*(**byte)(unsafe.Pointer(&dst)), len(dst)*4), byte(src))
+	memset(unsafecast.Int32ToBytes(dst), byte(src))
 }
 
 //go:noescape
diff --git a/column_buffer_go18.go b/column_buffer_go18.go
index 8f01d8f..e919f14 100644
--- a/column_buffer_go18.go
+++ b/column_buffer_go18.go
@@ -8,6 +8,7 @@ import (
 	"unsafe"
 
 	"github.com/segmentio/parquet-go/deprecated"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 // writeRowsFunc is the type of functions that apply rows to a set of column
@@ -370,8 +371,8 @@ func writeRowsFuncOfMap(t reflect.Type, schema *Schema, path columnPath) writeRo
 					mapKey.SetIterKey(it)
 					mapValue.SetIterValue(it)
 
-					k := array{ptr: addressOf(mapKey), len: 1}
-					v := array{ptr: addressOf(mapValue), len: 1}
+					k := makeArray(unsafecast.PointerOfValue(mapKey), 1)
+					v := makeArray(unsafecast.PointerOfValue(mapValue), 1)
 
 					if err := writeKeyValues(columns, k, v, elemLevels); err != nil {
 						return err
@@ -385,7 +386,3 @@ func writeRowsFuncOfMap(t reflect.Type, schema *Schema, path columnPath) writeRo
 		return nil
 	}
 }
-
-func addressOf(v reflect.Value) unsafe.Pointer {
-	return (*[2]unsafe.Pointer)(unsafe.Pointer(&v))[1]
-}
diff --git a/column_index.go b/column_index.go
index 528ce3e..3e1ee7a 100644
--- a/column_index.go
+++ b/column_index.go
@@ -5,6 +5,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/format"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 type ColumnIndex interface {
@@ -321,8 +322,8 @@ func (i *booleanColumnIndexer) IndexPage(numValues, numNulls int64, min, max Val
 
 func (i *booleanColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.BoolToBytes(i.minValues), 1),
-		splitFixedLenByteArrays(bits.BoolToBytes(i.maxValues), 1),
+		splitFixedLenByteArrays(unsafecast.BoolToBytes(i.minValues), 1),
+		splitFixedLenByteArrays(unsafecast.BoolToBytes(i.maxValues), 1),
 		bits.OrderOfBool(i.minValues),
 		bits.OrderOfBool(i.maxValues),
 	)
@@ -352,8 +353,8 @@ func (i *int32ColumnIndexer) IndexPage(numValues, numNulls int64, min, max Value
 
 func (i *int32ColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Int32ToBytes(i.minValues), 4),
-		splitFixedLenByteArrays(bits.Int32ToBytes(i.maxValues), 4),
+		splitFixedLenByteArrays(unsafecast.Int32ToBytes(i.minValues), 4),
+		splitFixedLenByteArrays(unsafecast.Int32ToBytes(i.maxValues), 4),
 		bits.OrderOfInt32(i.minValues),
 		bits.OrderOfInt32(i.maxValues),
 	)
@@ -383,8 +384,8 @@ func (i *int64ColumnIndexer) IndexPage(numValues, numNulls int64, min, max Value
 
 func (i *int64ColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Int64ToBytes(i.minValues), 8),
-		splitFixedLenByteArrays(bits.Int64ToBytes(i.maxValues), 8),
+		splitFixedLenByteArrays(unsafecast.Int64ToBytes(i.minValues), 8),
+		splitFixedLenByteArrays(unsafecast.Int64ToBytes(i.maxValues), 8),
 		bits.OrderOfInt64(i.minValues),
 		bits.OrderOfInt64(i.maxValues),
 	)
@@ -445,8 +446,8 @@ func (i *floatColumnIndexer) IndexPage(numValues, numNulls int64, min, max Value
 
 func (i *floatColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Float32ToBytes(i.minValues), 4),
-		splitFixedLenByteArrays(bits.Float32ToBytes(i.maxValues), 4),
+		splitFixedLenByteArrays(unsafecast.Float32ToBytes(i.minValues), 4),
+		splitFixedLenByteArrays(unsafecast.Float32ToBytes(i.maxValues), 4),
 		bits.OrderOfFloat32(i.minValues),
 		bits.OrderOfFloat32(i.maxValues),
 	)
@@ -476,8 +477,8 @@ func (i *doubleColumnIndexer) IndexPage(numValues, numNulls int64, min, max Valu
 
 func (i *doubleColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Float64ToBytes(i.minValues), 8),
-		splitFixedLenByteArrays(bits.Float64ToBytes(i.maxValues), 8),
+		splitFixedLenByteArrays(unsafecast.Float64ToBytes(i.minValues), 8),
+		splitFixedLenByteArrays(unsafecast.Float64ToBytes(i.maxValues), 8),
 		bits.OrderOfFloat64(i.minValues),
 		bits.OrderOfFloat64(i.maxValues),
 	)
@@ -593,8 +594,8 @@ func (i *uint32ColumnIndexer) IndexPage(numValues, numNulls int64, min, max Valu
 
 func (i *uint32ColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Uint32ToBytes(i.minValues), 4),
-		splitFixedLenByteArrays(bits.Uint32ToBytes(i.maxValues), 4),
+		splitFixedLenByteArrays(unsafecast.Uint32ToBytes(i.minValues), 4),
+		splitFixedLenByteArrays(unsafecast.Uint32ToBytes(i.maxValues), 4),
 		bits.OrderOfUint32(i.minValues),
 		bits.OrderOfUint32(i.maxValues),
 	)
@@ -624,8 +625,8 @@ func (i *uint64ColumnIndexer) IndexPage(numValues, numNulls int64, min, max Valu
 
 func (i *uint64ColumnIndexer) ColumnIndex() format.ColumnIndex {
 	return i.columnIndex(
-		splitFixedLenByteArrays(bits.Uint64ToBytes(i.minValues), 8),
-		splitFixedLenByteArrays(bits.Uint64ToBytes(i.maxValues), 8),
+		splitFixedLenByteArrays(unsafecast.Uint64ToBytes(i.minValues), 8),
+		splitFixedLenByteArrays(unsafecast.Uint64ToBytes(i.maxValues), 8),
 		bits.OrderOfUint64(i.minValues),
 		bits.OrderOfUint64(i.maxValues),
 	)
@@ -658,8 +659,8 @@ func (i *be128ColumnIndexer) IndexPage(numValues, numNulls int64, min, max Value
 }
 
 func (i *be128ColumnIndexer) ColumnIndex() format.ColumnIndex {
-	minValues := splitFixedLenByteArrays(bits.Uint128ToBytes(i.minValues), 16)
-	maxValues := splitFixedLenByteArrays(bits.Uint128ToBytes(i.maxValues), 16)
+	minValues := splitFixedLenByteArrays(unsafecast.Uint128ToBytes(i.minValues), 16)
+	maxValues := splitFixedLenByteArrays(unsafecast.Uint128ToBytes(i.maxValues), 16)
 	return i.columnIndex(
 		minValues,
 		maxValues,
diff --git a/dictionary.go b/dictionary.go
index 47ec559..3404f8d 100644
--- a/dictionary.go
+++ b/dictionary.go
@@ -9,6 +9,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 const (
@@ -206,7 +207,7 @@ func newInt32Dictionary(typ Type, columnIndex int16, numValues int32, values []b
 	return &int32Dictionary{
 		int32Page: int32Page{
 			typ:         typ,
-			values:      bits.BytesToInt32(values)[:numValues],
+			values:      unsafecast.BytesToInt32(values)[:numValues],
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -282,7 +283,7 @@ func newInt64Dictionary(typ Type, columnIndex int16, numValues int32, values []b
 	return &int64Dictionary{
 		int64Page: int64Page{
 			typ:         typ,
-			values:      bits.BytesToInt64(values)[:numValues],
+			values:      unsafecast.BytesToInt64(values)[:numValues],
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -453,7 +454,7 @@ func newFloatDictionary(typ Type, columnIndex int16, numValues int32, values []b
 	return &floatDictionary{
 		floatPage: floatPage{
 			typ:         typ,
-			values:      bits.BytesToFloat32(values)[:numValues],
+			values:      unsafecast.BytesToFloat32(values)[:numValues],
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -529,7 +530,7 @@ func newDoubleDictionary(typ Type, columnIndex int16, numValues int32, values []
 	return &doubleDictionary{
 		doublePage: doublePage{
 			typ:         typ,
-			values:      bits.BytesToFloat64(values)[:numValues],
+			values:      unsafecast.BytesToFloat64(values)[:numValues],
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -677,7 +678,7 @@ func (d *byteArrayDictionary) Lookup(indexes []int32, values []Value) {
 func (d *byteArrayDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
 		base := d.index(indexes[0])
-		minValue := *(*string)(unsafe.Pointer(&base))
+		minValue := unsafecast.BytesToString(base)
 		maxValue := minValue
 		values := [64]string{}
 
@@ -794,7 +795,7 @@ func (d *fixedLenByteArrayDictionary) Lookup(indexes []int32, values []Value) {
 func (d *fixedLenByteArrayDictionary) Bounds(indexes []int32) (min, max Value) {
 	if len(indexes) > 0 {
 		base := d.index(indexes[0])
-		minValue := *(*string)(unsafe.Pointer(&base))
+		minValue := unsafecast.BytesToString(base)
 		maxValue := minValue
 		values := [64]string{}
 
@@ -840,7 +841,7 @@ func newUint32Dictionary(typ Type, columnIndex int16, numValues int32, data []by
 	return &uint32Dictionary{
 		uint32Page: uint32Page{
 			typ:         typ,
-			values:      bits.BytesToUint32(data)[:numValues],
+			values:      unsafecast.BytesToUint32(data)[:numValues],
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -916,7 +917,7 @@ func newUint64Dictionary(typ Type, columnIndex int16, numValues int32, data []by
 	return &uint64Dictionary{
 		uint64Page: uint64Page{
 			typ:         typ,
-			values:      bits.BytesToUint64(data),
+			values:      unsafecast.BytesToUint64(data),
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -992,7 +993,7 @@ func newBE128Dictionary(typ Type, columnIndex int16, numValues int32, data []byt
 	return &be128Dictionary{
 		be128Page: be128Page{
 			typ:         typ,
-			values:      bits.BytesToUint128(data),
+			values:      unsafecast.BytesToUint128(data),
 			columnIndex: ^columnIndex,
 		},
 	}
@@ -1117,7 +1118,7 @@ func newIndexedPage(typ *indexedType, columnIndex int16, numValues int32, values
 
 	return &indexedPage{
 		typ:         typ,
-		values:      bits.BytesToInt32(values[:size]),
+		values:      unsafecast.BytesToInt32(values[:size]),
 		columnIndex: ^columnIndex,
 	}
 }
@@ -1140,7 +1141,7 @@ func (page *indexedPage) RepetitionLevels() []byte { return nil }
 
 func (page *indexedPage) DefinitionLevels() []byte { return nil }
 
-func (page *indexedPage) Data() []byte { return bits.Int32ToBytes(page.values) }
+func (page *indexedPage) Data() []byte { return unsafecast.Int32ToBytes(page.values) }
 
 func (page *indexedPage) Values() ValueReader { return &indexedPageValues{page: page} }
 
diff --git a/dictionary_amd64.go b/dictionary_amd64.go
index 0bf0ab4..d639a31 100644
--- a/dictionary_amd64.go
+++ b/dictionary_amd64.go
@@ -3,9 +3,7 @@
 package parquet
 
 import (
-	"unsafe"
-
-	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 //go:noescape
@@ -46,25 +44,25 @@ func dictionaryLookupFixedLenByteArrayPointer(dict []byte, len int, indexes []in
 
 func (d *int32Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := *(*[]uint32)(unsafe.Pointer(&d.values))
+	dict := unsafecast.Int32ToUint32(d.values)
 	dictionaryLookup32bits(dict, indexes, rows, size, offset).check()
 }
 
 func (d *int64Dictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := *(*[]uint64)(unsafe.Pointer(&d.values))
+	dict := unsafecast.Int64ToUint64(d.values)
 	dictionaryLookup64bits(dict, indexes, rows, size, offset).check()
 }
 
 func (d *floatDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := *(*[]uint32)(unsafe.Pointer(&d.values))
+	dict := unsafecast.Float32ToUint32(d.values)
 	dictionaryLookup32bits(dict, indexes, rows, size, offset).check()
 }
 
 func (d *doubleDictionary) lookup(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := *(*[]uint64)(unsafe.Pointer(&d.values))
+	dict := unsafecast.Float64ToUint64(d.values)
 	dictionaryLookup64bits(dict, indexes, rows, size, offset).check()
 }
 
@@ -90,13 +88,13 @@ func (d *uint64Dictionary) lookup(indexes []int32, rows array, size, offset uint
 
 func (d *be128Dictionary) lookupString(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := bits.Uint128ToBytes(d.values)
+	dict := unsafecast.Uint128ToBytes(d.values)
 	dictionaryLookupFixedLenByteArrayString(dict, 16, indexes, rows, size, offset).check()
 }
 
 func (d *be128Dictionary) lookupPointer(indexes []int32, rows array, size, offset uintptr) {
 	checkLookupIndexBounds(indexes, rows)
-	dict := bits.Uint128ToBytes(d.values)
+	dict := unsafecast.Uint128ToBytes(d.values)
 	dictionaryLookupFixedLenByteArrayPointer(dict, 16, indexes, rows, size, offset).check()
 }
 
diff --git a/encoding/rle/dictionary.go b/encoding/rle/dictionary.go
index ef0a55b..e98e730 100644
--- a/encoding/rle/dictionary.go
+++ b/encoding/rle/dictionary.go
@@ -4,6 +4,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/format"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 type DictionaryEncoding struct {
@@ -22,7 +23,7 @@ func (e *DictionaryEncoding) EncodeInt32(dst, src []byte) ([]byte, error) {
 	if (len(src) % 4) != 0 {
 		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT32", len(src))
 	}
-	src32 := bits.BytesToInt32(src)
+	src32 := unsafecast.BytesToInt32(src)
 	bitWidth := bits.MaxLen32(src32)
 	dst = append(dst[:0], byte(bitWidth))
 	dst, err := encodeInt32(dst, src32, uint(bitWidth))
diff --git a/encoding/rle/rle.go b/encoding/rle/rle.go
index a7e3363..6e2fc79 100644
--- a/encoding/rle/rle.go
+++ b/encoding/rle/rle.go
@@ -15,6 +15,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding"
 	"github.com/segmentio/parquet-go/format"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 const (
@@ -59,7 +60,7 @@ func (e *Encoding) EncodeInt32(dst, src []byte) ([]byte, error) {
 	if (len(src) % 4) != 0 {
 		return dst[:0], encoding.ErrEncodeInvalidInputSize(e, "INT32", len(src))
 	}
-	dst, err := encodeInt32(dst[:0], bits.BytesToInt32(src), uint(e.BitWidth))
+	dst, err := encodeInt32(dst[:0], unsafecast.BytesToInt32(src), uint(e.BitWidth))
 	return dst, e.wrap(err)
 }
 
@@ -197,7 +198,7 @@ func encodeInt32(dst []byte, src []int32, bitWidth uint) ([]byte, error) {
 		return dst, errEncodeInvalidBitWidth("INT32", bitWidth)
 	}
 	if bitWidth == 0 {
-		if !isZeroInt32(src) {
+		if !isZero(unsafecast.Int32ToBytes(src)) {
 			return dst, errEncodeInvalidBitWidth("INT32", bitWidth)
 		}
 		return appendUvarint(dst, uint64(len(src))<<1), nil
@@ -499,14 +500,6 @@ func isOnes(data []byte) bool {
 	return count(data, 0xFF) == len(data)
 }
 
-func isZeroInt8(data []int8) bool {
-	return isZero(bits.Int8ToBytes(data))
-}
-
-func isZeroInt32(data []int32) bool {
-	return isZero(bits.Int32ToBytes(data))
-}
-
 func resize(buf []byte, size int) []byte {
 	if cap(buf) < size {
 		return grow(buf, size)
diff --git a/internal/bits/max_default.go b/internal/bits/max_default.go
index 0257722..b5a8ffc 100644
--- a/internal/bits/max_default.go
+++ b/internal/bits/max_default.go
@@ -2,7 +2,11 @@
 
 package bits
 
-import "encoding/binary"
+import (
+	"encoding/binary"
+
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+)
 
 func maxBool(data []bool) (max bool) {
 	return len(data) > 0 && !boolEqualAll(data, false)
@@ -88,7 +92,7 @@ func maxFloat64(data []float64) (max float64) {
 
 func maxBE128(data []byte) (min []byte) {
 	if len(data) > 0 {
-		be128 := BytesToUint128(data)
+		be128 := unsafecast.BytesToUint128(data)
 		m := binary.BigEndian.Uint64(be128[0][:8])
 		j := 0
 		for i := 1; i < len(be128); i++ {
diff --git a/internal/bits/max_go18.go b/internal/bits/max_go18.go
index 149926e..c6257bb 100644
--- a/internal/bits/max_go18.go
+++ b/internal/bits/max_go18.go
@@ -39,7 +39,7 @@ func max[T ordered](data []T) (max T) {
 
 func maxBE128(data []byte) (min []byte) {
 	if len(data) > 0 {
-		be128 := unsafecast.BytesToSlice[uint128](data)
+		be128 := unsafecast.Slice[uint128](data)
 		m := binary.BigEndian.Uint64(be128[0][:8])
 		j := 0
 		for i := 1; i < len(be128); i++ {
diff --git a/internal/bits/min_default.go b/internal/bits/min_default.go
index 934ca18..a74ed03 100644
--- a/internal/bits/min_default.go
+++ b/internal/bits/min_default.go
@@ -2,7 +2,11 @@
 
 package bits
 
-import "encoding/binary"
+import (
+	"encoding/binary"
+
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+)
 
 func minBool(data []bool) (min bool) {
 	return boolEqualAll(data, true)
@@ -88,7 +92,7 @@ func minFloat64(data []float64) (min float64) {
 
 func minBE128(data []byte) (min []byte) {
 	if len(data) > 0 {
-		be128 := BytesToUint128(data)
+		be128 := unsafecast.BytesToUint128(data)
 		m := binary.BigEndian.Uint64(be128[0][:8])
 		j := 0
 		for i := 1; i < len(be128); i++ {
diff --git a/internal/bits/min_go18.go b/internal/bits/min_go18.go
index 0bd9796..1b8c454 100644
--- a/internal/bits/min_go18.go
+++ b/internal/bits/min_go18.go
@@ -37,7 +37,7 @@ func min[T ordered](data []T) (min T) {
 
 func minBE128(data []byte) (min []byte) {
 	if len(data) > 0 {
-		be128 := unsafecast.BytesToSlice[uint128](data)
+		be128 := unsafecast.Slice[uint128](data)
 		m := binary.BigEndian.Uint64(be128[0][:8])
 		j := 0
 		for i := 1; i < len(be128); i++ {
diff --git a/internal/bits/minmax_default.go b/internal/bits/minmax_default.go
index 672a326..a9e3981 100644
--- a/internal/bits/minmax_default.go
+++ b/internal/bits/minmax_default.go
@@ -2,7 +2,11 @@
 
 package bits
 
-import "encoding/binary"
+import (
+	"encoding/binary"
+
+	"github.com/segmentio/parquet-go/internal/unsafecast"
+)
 
 func boolEqualAll(data []bool, value bool) bool {
 	for _, v := range data {
@@ -131,7 +135,7 @@ func minMaxFloat64(data []float64) (min, max float64) {
 
 func minMaxBE128(data []byte) (min, max []byte) {
 	if len(data) > 0 {
-		be128 := BytesToUint128(data)
+		be128 := unsafecast.BytesToUint128(data)
 		minHi := binary.BigEndian.Uint64(be128[0][:8])
 		maxHi := minHi
 		minIndex := 0
diff --git a/internal/bits/minmax_go18.go b/internal/bits/minmax_go18.go
index 4109628..79a43a1 100644
--- a/internal/bits/minmax_go18.go
+++ b/internal/bits/minmax_go18.go
@@ -62,7 +62,7 @@ func minmax[T ordered](data []T) (min, max T) {
 
 func minMaxBE128(data []byte) (min, max []byte) {
 	if len(data) > 0 {
-		be128 := unsafecast.BytesToSlice[uint128](data)
+		be128 := unsafecast.Slice[uint128](data)
 		minHi := binary.BigEndian.Uint64(be128[0][:8])
 		maxHi := minHi
 		minIndex := 0
diff --git a/internal/bits/order.go b/internal/bits/order.go
index 567145d..49e5a1c 100644
--- a/internal/bits/order.go
+++ b/internal/bits/order.go
@@ -2,6 +2,8 @@ package bits
 
 import (
 	"bytes"
+
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func OrderOfBool(data []bool) int {
@@ -34,14 +36,14 @@ func OrderOfBool(data []bool) int {
 }
 
 func streakOfTrue(data []bool) int {
-	if i := bytes.IndexByte(BoolToBytes(data), 0); i >= 0 {
+	if i := bytes.IndexByte(unsafecast.BoolToBytes(data), 0); i >= 0 {
 		return i
 	}
 	return len(data)
 }
 
 func streakOfFalse(data []bool) int {
-	if i := bytes.IndexByte(BoolToBytes(data), 1); i >= 0 {
+	if i := bytes.IndexByte(unsafecast.BoolToBytes(data), 1); i >= 0 {
 		return i
 	}
 	return len(data)
diff --git a/internal/bits/unsafe_go18.go b/internal/bits/unsafe_go18.go
deleted file mode 100644
index de13fda..0000000
--- a/internal/bits/unsafe_go18.go
+++ /dev/null
@@ -1,64 +0,0 @@
-//go:build go1.18
-
-package bits
-
-import "github.com/segmentio/parquet-go/internal/unsafecast"
-
-// TODO: remove these functions and use the internal/unsafecast package instead when
-// we drop support for Go 1.17.
-
-func BoolToBytes(data []bool) []byte { return unsafecast.SliceToBytes(data) }
-
-func Int8ToBytes(data []int8) []byte { return unsafecast.SliceToBytes(data) }
-
-func Int16ToBytes(data []int16) []byte { return unsafecast.SliceToBytes(data) }
-
-func Int32ToBytes(data []int32) []byte { return unsafecast.SliceToBytes(data) }
-
-func Int64ToBytes(data []int64) []byte { return unsafecast.SliceToBytes(data) }
-
-func Float32ToBytes(data []float32) []byte { return unsafecast.SliceToBytes(data) }
-
-func Float64ToBytes(data []float64) []byte { return unsafecast.SliceToBytes(data) }
-
-func Int16ToUint16(data []int16) []uint16 { return unsafecast.Slice[uint16](data) }
-
-func Int32ToUint32(data []int32) []uint32 { return unsafecast.Slice[uint32](data) }
-
-func Int64ToUint64(data []int64) []uint64 { return unsafecast.Slice[uint64](data) }
-
-func Float32ToUint32(data []float32) []uint32 { return unsafecast.Slice[uint32](data) }
-
-func Float64ToUint64(data []float64) []uint64 { return unsafecast.Slice[uint64](data) }
-
-func Uint32ToBytes(data []uint32) []byte { return unsafecast.SliceToBytes(data) }
-
-func Uint64ToBytes(data []uint64) []byte { return unsafecast.SliceToBytes(data) }
-
-func Uint128ToBytes(data [][16]byte) []byte { return unsafecast.SliceToBytes(data) }
-
-func Uint32ToInt32(data []uint32) []int32 { return unsafecast.Slice[int32](data) }
-
-func Uint64ToInt64(data []uint64) []int64 { return unsafecast.Slice[int64](data) }
-
-func BytesToBool(data []byte) []bool { return unsafecast.BytesToSlice[bool](data) }
-
-func BytesToInt8(data []byte) []int8 { return unsafecast.BytesToSlice[int8](data) }
-
-func BytesToInt16(data []byte) []int16 { return unsafecast.BytesToSlice[int16](data) }
-
-func BytesToInt32(data []byte) []int32 { return unsafecast.BytesToSlice[int32](data) }
-
-func BytesToInt64(data []byte) []int64 { return unsafecast.BytesToSlice[int64](data) }
-
-func BytesToUint32(data []byte) []uint32 { return unsafecast.BytesToSlice[uint32](data) }
-
-func BytesToUint64(data []byte) []uint64 { return unsafecast.BytesToSlice[uint64](data) }
-
-func BytesToUint128(data []byte) [][16]byte { return unsafecast.BytesToSlice[uint128](data) }
-
-func BytesToFloat32(data []byte) []float32 { return unsafecast.BytesToSlice[float32](data) }
-
-func BytesToFloat64(data []byte) []float64 { return unsafecast.BytesToSlice[float64](data) }
-
-func BytesToString(data []byte) string { return unsafecast.BytesToString(data) }
diff --git a/internal/unsafecast/unsafecast.go b/internal/unsafecast/unsafecast.go
deleted file mode 100644
index 1efeeab..0000000
--- a/internal/unsafecast/unsafecast.go
+++ /dev/null
@@ -1,76 +0,0 @@
-//go:build go1.18
-
-// Package unsafecast exposes functions to bypass the Go type system and perform
-// conversions between types that would otherwise not be possible.
-//
-// The functions of this package are mostly useful as optimizations to avoid
-// memory copies when converting between compatible memory layouts; for example,
-// casting a [][16]byte to a []byte in order to use functions of the standard
-// bytes package on the slices.
-//
-//		With great power comes great responsibility.
-//
-package unsafecast
-
-import "unsafe"
-
-// The slice type represents the memory layout of slices in Go. It is similar to
-// reflect.SliceHeader but uses a unsafe.Pointer instead of uintptr to for the
-// backing array to allow the garbage collector to track track the reference.
-type slice struct {
-	ptr unsafe.Pointer
-	len int
-	cap int
-}
-
-// Slice converts the data slice of type []From to a slice of type []To sharing
-// the same backing array. The length and capacity of the returned slice are
-// scaled according to the size difference between the source and destination
-// types.
-//
-// Note that the function does not perform any checks to ensure that the memory
-// layouts of the types are compatible, it is possible to cause memory
-// corruption if the layouts mismatch (e.g. the pointers in the From are different
-// than the pointers in To).
-func Slice[To, From any](data []From) []To {
-	// This function could use unsafe.Slice but it would drop the capacity
-	// information, so instead we implement the type conversion.
-	var zf From
-	var zt To
-	var s = (*slice)(unsafe.Pointer(&data))
-	s.len = int((uintptr(s.len) * unsafe.Sizeof(zf)) / unsafe.Sizeof(zt))
-	s.cap = int((uintptr(s.cap) * unsafe.Sizeof(zf)) / unsafe.Sizeof(zt))
-	return *(*[]To)(unsafe.Pointer(s))
-}
-
-// SliceToBytes is a specialization of the Slice function converting any slice
-// to a byte slice.
-func SliceToBytes[T any](data []T) []byte {
-	return Slice[byte](data)
-}
-
-// BytesToSlice is a specialization fo the Slice function for the case where
-// converting a byte slice to a different type.
-func BytesToSlice[T any](data []byte) []T {
-	return Slice[T](data)
-}
-
-// BytesToString converts a byte slice to a string value. The returned string
-// shares the backing array of the byte slice.
-//
-// Programs using this function are responsible for ensuring that the data slice
-// is not modified while the returned string is in use, otherwise the guarantee
-// of immutability of Go string values will be violated, resulting in undefined
-// behavior.
-func BytesToString(data []byte) string {
-	return *(*string)(unsafe.Pointer(&data))
-}
-
-// StringToBytes applies the inverse conversion of BytesToString.
-func StringToBytes(data string) []byte {
-	return *(*[]byte)(unsafe.Pointer(&slice{
-		ptr: *(*unsafe.Pointer)(unsafe.Pointer(&data)),
-		len: len(data),
-		cap: len(data),
-	}))
-}
diff --git a/internal/bits/unsafe.go b/internal/unsafecast/unsafecast_go17.go
similarity index 90%
rename from internal/bits/unsafe.go
rename to internal/unsafecast/unsafecast_go17.go
index 58c76ff..41dda85 100644
--- a/internal/bits/unsafe.go
+++ b/internal/unsafecast/unsafecast_go17.go
@@ -1,8 +1,23 @@
 //go:build !go1.18
 
-package bits
+package unsafecast
 
-import "unsafe"
+import (
+	"reflect"
+	"unsafe"
+)
+
+func AddressOfBytes(data []byte) *byte {
+	return *(**byte)(unsafe.Pointer(&data))
+}
+
+func AddressOfString(data string) *byte {
+	return *(**byte)(unsafe.Pointer(&data))
+}
+
+func PointerOfValue(value reflect.Value) unsafe.Pointer {
+	return (*[2]unsafe.Pointer)(unsafe.Pointer(&value))[1]
+}
 
 func BoolToBytes(data []bool) []byte {
 	return unsafe.Slice(*(**byte)(unsafe.Pointer(&data)), len(data))
diff --git a/internal/unsafecast/unsafecast_go18.go b/internal/unsafecast/unsafecast_go18.go
new file mode 100644
index 0000000..2ec70a8
--- /dev/null
+++ b/internal/unsafecast/unsafecast_go18.go
@@ -0,0 +1,163 @@
+//go:build go1.18
+
+// Package unsafecast exposes functions to bypass the Go type system and perform
+// conversions between types that would otherwise not be possible.
+//
+// The functions of this package are mostly useful as optimizations to avoid
+// memory copies when converting between compatible memory layouts; for example,
+// casting a [][16]byte to a []byte in order to use functions of the standard
+// bytes package on the slices.
+//
+//		With great power comes great responsibility.
+//
+package unsafecast
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+// AddressOf returns the address to the first element in data, even if the slice
+// has length zero.
+func AddressOf[T any](data []T) *T {
+	return *(**T)(unsafe.Pointer(&data))
+}
+
+// AddressOfBytes returns the address of the first byte in data.
+func AddressOfBytes(data []byte) *byte {
+	return AddressOf(data)
+}
+
+// AddressOfString returns the address of the first byte in data.
+func AddressOfString(data string) *byte {
+	return *(**byte)(unsafe.Pointer(&data))
+}
+
+// PointerOf is like AddressOf but returns an unsafe.Pointer, losing type
+// information about the underlying data.
+func PointerOf[T any](data []T) unsafe.Pointer {
+	return unsafe.Pointer(AddressOf(data))
+}
+
+// PointerOfString is like AddressOfString but returns an unsafe.Pointer, losing
+// type information about the underlying data.
+func PointerOfString(data string) unsafe.Pointer {
+	return unsafe.Pointer(AddressOfString(data))
+}
+
+// PointerOfValue returns the address of the object packed in the given value.
+//
+// This function is like value.UnsafePointer but works for any underlying type,
+// bypassing the safety checks done by the reflect package.
+func PointerOfValue(value reflect.Value) unsafe.Pointer {
+	return (*[2]unsafe.Pointer)(unsafe.Pointer(&value))[1]
+}
+
+// The slice type represents the memory layout of slices in Go. It is similar to
+// reflect.SliceHeader but uses a unsafe.Pointer instead of uintptr to for the
+// backing array to allow the garbage collector to track track the reference.
+type slice struct {
+	ptr unsafe.Pointer
+	len int
+	cap int
+}
+
+// Slice converts the data slice of type []From to a slice of type []To sharing
+// the same backing array. The length and capacity of the returned slice are
+// scaled according to the size difference between the source and destination
+// types.
+//
+// Note that the function does not perform any checks to ensure that the memory
+// layouts of the types are compatible, it is possible to cause memory
+// corruption if the layouts mismatch (e.g. the pointers in the From are different
+// than the pointers in To).
+func Slice[To, From any](data []From) []To {
+	// This function could use unsafe.Slice but it would drop the capacity
+	// information, so instead we implement the type conversion.
+	var zf From
+	var zt To
+	var s = (*slice)(unsafe.Pointer(&data))
+	s.len = int((uintptr(s.len) * unsafe.Sizeof(zf)) / unsafe.Sizeof(zt))
+	s.cap = int((uintptr(s.cap) * unsafe.Sizeof(zf)) / unsafe.Sizeof(zt))
+	return *(*[]To)(unsafe.Pointer(s))
+}
+
+// BytesToString converts a byte slice to a string value. The returned string
+// shares the backing array of the byte slice.
+//
+// Programs using this function are responsible for ensuring that the data slice
+// is not modified while the returned string is in use, otherwise the guarantee
+// of immutability of Go string values will be violated, resulting in undefined
+// behavior.
+func BytesToString(data []byte) string {
+	return *(*string)(unsafe.Pointer(&data))
+}
+
+// StringToBytes applies the inverse conversion of BytesToString.
+func StringToBytes(data string) []byte {
+	return *(*[]byte)(unsafe.Pointer(&slice{
+		ptr: PointerOfString(data),
+		len: len(data),
+		cap: len(data),
+	}))
+}
+
+// -----------------------------------------------------------------------------
+// TODO: the functions below are used for backward compatibility with Go 1.17
+// where generics weren't available. We should remove them and inline calls to
+// unsafecast.Slice when we change our minimum supported Go version to 1.18.
+// -----------------------------------------------------------------------------
+
+func BoolToBytes(data []bool) []byte { return Slice[byte](data) }
+
+func Int8ToBytes(data []int8) []byte { return Slice[byte](data) }
+
+func Int16ToBytes(data []int16) []byte { return Slice[byte](data) }
+
+func Int32ToBytes(data []int32) []byte { return Slice[byte](data) }
+
+func Int64ToBytes(data []int64) []byte { return Slice[byte](data) }
+
+func Float32ToBytes(data []float32) []byte { return Slice[byte](data) }
+
+func Float64ToBytes(data []float64) []byte { return Slice[byte](data) }
+
+func Uint32ToBytes(data []uint32) []byte { return Slice[byte](data) }
+
+func Uint64ToBytes(data []uint64) []byte { return Slice[byte](data) }
+
+func Uint128ToBytes(data [][16]byte) []byte { return Slice[byte](data) }
+
+func Int16ToUint16(data []int16) []uint16 { return Slice[uint16](data) }
+
+func Int32ToUint32(data []int32) []uint32 { return Slice[uint32](data) }
+
+func Int64ToUint64(data []int64) []uint64 { return Slice[uint64](data) }
+
+func Float32ToUint32(data []float32) []uint32 { return Slice[uint32](data) }
+
+func Float64ToUint64(data []float64) []uint64 { return Slice[uint64](data) }
+
+func Uint32ToInt32(data []uint32) []int32 { return Slice[int32](data) }
+
+func Uint64ToInt64(data []uint64) []int64 { return Slice[int64](data) }
+
+func BytesToBool(data []byte) []bool { return Slice[bool](data) }
+
+func BytesToInt8(data []byte) []int8 { return Slice[int8](data) }
+
+func BytesToInt16(data []byte) []int16 { return Slice[int16](data) }
+
+func BytesToInt32(data []byte) []int32 { return Slice[int32](data) }
+
+func BytesToInt64(data []byte) []int64 { return Slice[int64](data) }
+
+func BytesToUint32(data []byte) []uint32 { return Slice[uint32](data) }
+
+func BytesToUint64(data []byte) []uint64 { return Slice[uint64](data) }
+
+func BytesToUint128(data []byte) [][16]byte { return Slice[[16]byte](data) }
+
+func BytesToFloat32(data []byte) []float32 { return Slice[float32](data) }
+
+func BytesToFloat64(data []byte) []float64 { return Slice[float64](data) }
diff --git a/page.go b/page.go
index d63b06c..1a1ca86 100644
--- a/page.go
+++ b/page.go
@@ -9,6 +9,7 @@ import (
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 // Page values represent sequences of parquet values. From the Parquet
@@ -591,7 +592,7 @@ type int32Page struct {
 func newInt32Page(typ Type, columnIndex int16, numValues int32, values []byte) *int32Page {
 	return &int32Page{
 		typ:         typ,
-		values:      bits.BytesToInt32(values)[:numValues],
+		values:      unsafecast.BytesToInt32(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -614,7 +615,7 @@ func (page *int32Page) RepetitionLevels() []byte { return nil }
 
 func (page *int32Page) DefinitionLevels() []byte { return nil }
 
-func (page *int32Page) Data() []byte { return bits.Int32ToBytes(page.values) }
+func (page *int32Page) Data() []byte { return unsafecast.Int32ToBytes(page.values) }
 
 func (page *int32Page) Values() ValueReader { return &int32PageValues{page: page} }
 
@@ -666,7 +667,7 @@ type int64Page struct {
 func newInt64Page(typ Type, columnIndex int16, numValues int32, values []byte) *int64Page {
 	return &int64Page{
 		typ:         typ,
-		values:      bits.BytesToInt64(values)[:numValues],
+		values:      unsafecast.BytesToInt64(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -689,7 +690,7 @@ func (page *int64Page) RepetitionLevels() []byte { return nil }
 
 func (page *int64Page) DefinitionLevels() []byte { return nil }
 
-func (page *int64Page) Data() []byte { return bits.Int64ToBytes(page.values) }
+func (page *int64Page) Data() []byte { return unsafecast.Int64ToBytes(page.values) }
 
 func (page *int64Page) Values() ValueReader { return &int64PageValues{page: page} }
 
@@ -818,7 +819,7 @@ type floatPage struct {
 func newFloatPage(typ Type, columnIndex int16, numValues int32, values []byte) *floatPage {
 	return &floatPage{
 		typ:         typ,
-		values:      bits.BytesToFloat32(values)[:numValues],
+		values:      unsafecast.BytesToFloat32(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -841,7 +842,7 @@ func (page *floatPage) RepetitionLevels() []byte { return nil }
 
 func (page *floatPage) DefinitionLevels() []byte { return nil }
 
-func (page *floatPage) Data() []byte { return bits.Float32ToBytes(page.values) }
+func (page *floatPage) Data() []byte { return unsafecast.Float32ToBytes(page.values) }
 
 func (page *floatPage) Values() ValueReader { return &floatPageValues{page: page} }
 
@@ -893,7 +894,7 @@ type doublePage struct {
 func newDoublePage(typ Type, columnIndex int16, numValues int32, values []byte) *doublePage {
 	return &doublePage{
 		typ:         typ,
-		values:      bits.BytesToFloat64(values)[:numValues],
+		values:      unsafecast.BytesToFloat64(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -916,7 +917,7 @@ func (page *doublePage) RepetitionLevels() []byte { return nil }
 
 func (page *doublePage) DefinitionLevels() []byte { return nil }
 
-func (page *doublePage) Data() []byte { return bits.Float64ToBytes(page.values) }
+func (page *doublePage) Data() []byte { return unsafecast.Float64ToBytes(page.values) }
 
 func (page *doublePage) Values() ValueReader { return &doublePageValues{page: page} }
 
@@ -1235,7 +1236,7 @@ type uint32Page struct {
 func newUint32Page(typ Type, columnIndex int16, numValues int32, values []byte) *uint32Page {
 	return &uint32Page{
 		typ:         typ,
-		values:      bits.BytesToUint32(values)[:numValues],
+		values:      unsafecast.BytesToUint32(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -1258,7 +1259,7 @@ func (page *uint32Page) RepetitionLevels() []byte { return nil }
 
 func (page *uint32Page) DefinitionLevels() []byte { return nil }
 
-func (page *uint32Page) Data() []byte { return bits.Uint32ToBytes(page.values) }
+func (page *uint32Page) Data() []byte { return unsafecast.Uint32ToBytes(page.values) }
 
 func (page *uint32Page) Values() ValueReader { return &uint32PageValues{page: page} }
 
@@ -1310,7 +1311,7 @@ type uint64Page struct {
 func newUint64Page(typ Type, columnIndex int16, numValues int32, values []byte) *uint64Page {
 	return &uint64Page{
 		typ:         typ,
-		values:      bits.BytesToUint64(values)[:numValues],
+		values:      unsafecast.BytesToUint64(values)[:numValues],
 		columnIndex: ^columnIndex,
 	}
 }
@@ -1333,7 +1334,7 @@ func (page *uint64Page) RepetitionLevels() []byte { return nil }
 
 func (page *uint64Page) DefinitionLevels() []byte { return nil }
 
-func (page *uint64Page) Data() []byte { return bits.Uint64ToBytes(page.values) }
+func (page *uint64Page) Data() []byte { return unsafecast.Uint64ToBytes(page.values) }
 
 func (page *uint64Page) Values() ValueReader { return &uint64PageValues{page: page} }
 
@@ -1391,7 +1392,7 @@ func newBE128Page(typ Type, columnIndex int16, numValues int32, data []byte) *be
 	}
 	return &be128Page{
 		typ:         typ,
-		values:      bits.BytesToUint128(data),
+		values:      unsafecast.BytesToUint128(data),
 		columnIndex: ^columnIndex,
 	}
 }
@@ -1414,7 +1415,7 @@ func (page *be128Page) RepetitionLevels() []byte { return nil }
 
 func (page *be128Page) DefinitionLevels() []byte { return nil }
 
-func (page *be128Page) Data() []byte { return bits.Uint128ToBytes(page.values) }
+func (page *be128Page) Data() []byte { return unsafecast.Uint128ToBytes(page.values) }
 
 func (page *be128Page) Values() ValueReader { return &be128PageValues{page: page} }
 
diff --git a/page_values.go b/page_values.go
index 857305e..c25f6da 100644
--- a/page_values.go
+++ b/page_values.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
-	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 type optionalPageValues struct {
@@ -138,7 +138,7 @@ type int32PageValues struct {
 }
 
 func (r *int32PageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadInt32s(bits.BytesToInt32(b))
+	n, err = r.ReadInt32s(unsafecast.BytesToInt32(b))
 	return 4 * n, err
 }
 
@@ -169,7 +169,7 @@ type int64PageValues struct {
 }
 
 func (r *int64PageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadInt64s(bits.BytesToInt64(b))
+	n, err = r.ReadInt64s(unsafecast.BytesToInt64(b))
 	return 8 * n, err
 }
 
@@ -231,7 +231,7 @@ type floatPageValues struct {
 }
 
 func (r *floatPageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadFloats(bits.BytesToFloat32(b))
+	n, err = r.ReadFloats(unsafecast.BytesToFloat32(b))
 	return 4 * n, err
 }
 
@@ -262,7 +262,7 @@ type doublePageValues struct {
 }
 
 func (r *doublePageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadDoubles(bits.BytesToFloat64(b))
+	n, err = r.ReadDoubles(unsafecast.BytesToFloat64(b))
 	return 8 * n, err
 }
 
@@ -385,7 +385,7 @@ type uint32PageValues struct {
 }
 
 func (r *uint32PageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadUint32s(bits.BytesToUint32(b))
+	n, err = r.ReadUint32s(unsafecast.BytesToUint32(b))
 	return 4 * n, err
 }
 
@@ -416,7 +416,7 @@ type uint64PageValues struct {
 }
 
 func (r *uint64PageValues) Read(b []byte) (n int, err error) {
-	n, err = r.ReadUint64s(bits.BytesToUint64(b))
+	n, err = r.ReadUint64s(unsafecast.BytesToUint64(b))
 	return 8 * n, err
 }
 
diff --git a/value.go b/value.go
index aebd728..a7e39e5 100644
--- a/value.go
+++ b/value.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/segmentio/parquet-go/deprecated"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 const (
@@ -329,11 +330,11 @@ func makeValueDouble(value float64) Value {
 }
 
 func makeValueBytes(kind Kind, value []byte) Value {
-	return makeValueByteArray(kind, *(**byte)(unsafe.Pointer(&value)), len(value))
+	return makeValueByteArray(kind, unsafecast.AddressOfBytes(value), len(value))
 }
 
 func makeValueString(kind Kind, value string) Value {
-	return makeValueByteArray(kind, *(**byte)(unsafe.Pointer(&value)), len(value))
+	return makeValueByteArray(kind, unsafecast.AddressOfString(value), len(value))
 }
 
 func makeValueFixedLenByteArray(v reflect.Value) Value {
@@ -348,9 +349,7 @@ func makeValueFixedLenByteArray(v reflect.Value) Value {
 		u.Elem().Set(v)
 		v = u
 	}
-	// TODO: unclear if the conversion to unsafe.Pointer from
-	// reflect.Value.Pointer is safe here.
-	return makeValueByteArray(FixedLenByteArray, (*byte)(unsafe.Pointer(v.Pointer())), t.Len())
+	return makeValueByteArray(FixedLenByteArray, (*byte)(unsafePointer(v)), t.Len())
 }
 
 func makeValueByteArray(kind Kind, data *byte, size int) Value {
@@ -546,7 +545,7 @@ func (v Value) String() string {
 		// underlying data to a new memory location. This is safe as long as the
 		// application respects the requirement to not mutate the byte slices
 		// returned when calling ByteArray.
-		return unsafeBytesToString(v.ByteArray())
+		return unsafecast.BytesToString(v.ByteArray())
 	default:
 		return "<null>"
 	}
@@ -573,7 +572,7 @@ func (v Value) Clone() Value {
 	switch k := v.Kind(); k {
 	case ByteArray, FixedLenByteArray:
 		b := copyBytes(v.ByteArray())
-		v.ptr = *(**byte)(unsafe.Pointer(&b))
+		v.ptr = unsafecast.AddressOfBytes(b)
 	}
 	return v
 }
@@ -682,7 +681,8 @@ func assignValue(dst reflect.Value, src Value) error {
 				// overhead we instead convert the reflect.Value holding the
 				// destination array into a byte slice which allows us to use
 				// a more efficient call to copy.
-				copy(unsafeByteArray(dst, len(v)), v)
+				d := unsafe.Slice((*byte)(unsafecast.PointerOfValue(dst)), len(v))
+				copy(d, v)
 				return nil
 			}
 		case reflect.Slice:
@@ -744,18 +744,6 @@ func copyBytes(b []byte) []byte {
 	return c
 }
 
-func unsafeBytesToString(b []byte) string {
-	return *(*string)(unsafe.Pointer(&b))
-}
-
-func unsafePointerOf(v reflect.Value) unsafe.Pointer {
-	return (*[2]unsafe.Pointer)(unsafe.Pointer(&v))[1]
-}
-
-func unsafeByteArray(v reflect.Value, n int) []byte {
-	return unsafe.Slice((*byte)(unsafePointerOf(v)), n)
-}
-
 // Equal returns true if v1 and v2 are equal.
 //
 // Values are considered equal if they are of the same physical type and hold
diff --git a/value_go17.go b/value_go17.go
new file mode 100644
index 0000000..8c891a0
--- /dev/null
+++ b/value_go17.go
@@ -0,0 +1,14 @@
+//go:build !go1.18
+
+package parquet
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+func unsafePointer(v reflect.Value) unsafe.Pointer {
+	// This may not have been a safe conversion but there were no better way
+	// prior to Go 1.18 and the introduction of reflect.Value.UnsafePointer.
+	return unsafe.Pointer(v.Pointer())
+}
diff --git a/value_go18.go b/value_go18.go
new file mode 100644
index 0000000..c07cf85
--- /dev/null
+++ b/value_go18.go
@@ -0,0 +1,14 @@
+//go:build go1.18
+
+package parquet
+
+import (
+	"reflect"
+	"unsafe"
+)
+
+// This function exists for backward compatiblity with the Go 1.17 build which
+// has a different implementation.
+//
+// TODO: remove when we drop support for Go versions prior to 1.18.
+func unsafePointer(v reflect.Value) unsafe.Pointer { return v.UnsafePointer() }
diff --git a/writer_go18.go b/writer_go18.go
index b596c70..397f705 100644
--- a/writer_go18.go
+++ b/writer_go18.go
@@ -119,7 +119,7 @@ func makeWriteFunc[T any](t reflect.Type, schema *Schema) writeFunc[T] {
 				w.columns[i] = c.columnBuffer
 			}
 		}
-		err = writeRows(w.columns, makeArray(rows), size, 0, columnLevels{})
+		err = writeRows(w.columns, makeArrayOf(rows), size, 0, columnLevels{})
 		if err == nil {
 			n = len(rows)
 		}


Test patch:
diff --git a/bloom_test.go b/bloom_test.go
index 81f4c58..cdbfc91 100644
--- a/bloom_test.go
+++ b/bloom_test.go
@@ -7,8 +7,8 @@ import (
 	"github.com/segmentio/parquet-go/bloom"
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
-	"github.com/segmentio/parquet-go/internal/bits"
 	"github.com/segmentio/parquet-go/internal/quick"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func TestSplitBlockFilter(t *testing.T) {
@@ -30,7 +30,7 @@ func TestSplitBlockFilter(t *testing.T) {
 			scenario: "BOOLEAN",
 			function: func(values []bool) bool {
 				filter := newFilter(len(values))
-				encoding.EncodeBoolean(filter.Bytes(), bits.BoolToBytes(values))
+				encoding.EncodeBoolean(filter.Bytes(), unsafecast.BoolToBytes(values))
 				for _, v := range values {
 					if !check(filter, ValueOf(v)) {
 						return false
@@ -44,7 +44,7 @@ func TestSplitBlockFilter(t *testing.T) {
 			scenario: "INT32",
 			function: func(values []int32) bool {
 				filter := newFilter(len(values))
-				encoding.EncodeInt32(filter.Bytes(), bits.Int32ToBytes(values))
+				encoding.EncodeInt32(filter.Bytes(), unsafecast.Int32ToBytes(values))
 				for _, v := range values {
 					if !check(filter, ValueOf(v)) {
 						return false
@@ -58,7 +58,7 @@ func TestSplitBlockFilter(t *testing.T) {
 			scenario: "INT64",
 			function: func(values []int64) bool {
 				filter := newFilter(len(values))
-				encoding.EncodeInt64(filter.Bytes(), bits.Int64ToBytes(values))
+				encoding.EncodeInt64(filter.Bytes(), unsafecast.Int64ToBytes(values))
 				for _, v := range values {
 					if !check(filter, ValueOf(v)) {
 						return false
@@ -86,7 +86,7 @@ func TestSplitBlockFilter(t *testing.T) {
 			scenario: "FLOAT",
 			function: func(values []float32) bool {
 				filter := newFilter(len(values))
-				encoding.EncodeFloat(filter.Bytes(), bits.Float32ToBytes(values))
+				encoding.EncodeFloat(filter.Bytes(), unsafecast.Float32ToBytes(values))
 				for _, v := range values {
 					if !check(filter, ValueOf(v)) {
 						return false
@@ -100,7 +100,7 @@ func TestSplitBlockFilter(t *testing.T) {
 			scenario: "DOUBLE",
 			function: func(values []float64) bool {
 				filter := newFilter(len(values))
-				encoding.EncodeDouble(filter.Bytes(), bits.Float64ToBytes(values))
+				encoding.EncodeDouble(filter.Bytes(), unsafecast.Float64ToBytes(values))
 				for _, v := range values {
 					if !check(filter, ValueOf(v)) {
 						return false
@@ -163,7 +163,7 @@ func BenchmarkSplitBlockFilter(b *testing.B) {
 		v[i] = r.Int63()
 	}
 
-	v64 := bits.Int64ToBytes(v)
+	v64 := unsafecast.Int64ToBytes(v)
 	for i := 0; i < b.N; i++ {
 		e.EncodeInt64(f, v64)
 	}
diff --git a/encoding/bytestreamsplit/bytestreamsplit_purego.go b/encoding/bytestreamsplit/bytestreamsplit_purego.go
index bad1bd1..1797228 100644
--- a/encoding/bytestreamsplit/bytestreamsplit_purego.go
+++ b/encoding/bytestreamsplit/bytestreamsplit_purego.go
@@ -2,7 +2,7 @@
 
 package bytestreamsplit
 
-import "github.com/segmentio/parquet-go/internal/bits"
+import "github.com/segmentio/parquet-go/internal/unsafecast"
 
 func encodeFloat(dst, src []byte) {
 	n := len(src) / 4
@@ -11,7 +11,7 @@ func encodeFloat(dst, src []byte) {
 	b2 := dst[2*n : 3*n]
 	b3 := dst[3*n : 4*n]
 
-	for i, v := range bits.BytesToUint32(src) {
+	for i, v := range unsafecast.BytesToUint32(src) {
 		b0[i] = byte(v >> 0)
 		b1[i] = byte(v >> 8)
 		b2[i] = byte(v >> 16)
@@ -30,7 +30,7 @@ func encodeDouble(dst, src []byte) {
 	b6 := dst[6*n : 7*n]
 	b7 := dst[7*n : 8*n]
 
-	for i, v := range bits.BytesToUint64(src) {
+	for i, v := range unsafecast.BytesToUint64(src) {
 		b0[i] = byte(v >> 0)
 		b1[i] = byte(v >> 8)
 		b2[i] = byte(v >> 16)
@@ -49,7 +49,7 @@ func decodeFloat(dst, src []byte) {
 	b2 := src[2*n : 3*n]
 	b3 := src[3*n : 4*n]
 
-	dst32 := bits.BytesToUint32(dst)
+	dst32 := unsafecast.BytesToUint32(dst)
 	for i := range dst32 {
 		dst32[i] = uint32(b0[i]) |
 			uint32(b1[i])<<8 |
@@ -69,7 +69,7 @@ func decodeDouble(dst, src []byte) {
 	b6 := src[6*n : 7*n]
 	b7 := src[7*n : 8*n]
 
-	dst64 := bits.BytesToUint64(dst)
+	dst64 := unsafecast.BytesToUint64(dst)
 	for i := range dst64 {
 		dst64[i] = uint64(b0[i]) |
 			uint64(b1[i])<<8 |
diff --git a/encoding/encoding_test.go b/encoding/encoding_test.go
index fed2acf..cb4424d 100644
--- a/encoding/encoding_test.go
+++ b/encoding/encoding_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/segmentio/parquet-go/encoding/plain"
 	"github.com/segmentio/parquet-go/encoding/rle"
 	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func repeatInt64(seq []int64, n int) []int64 {
@@ -305,7 +306,7 @@ func testLevelsEncoding(t *testing.T, e encoding.Encoding) {
 	values := []byte{}
 
 	for _, test := range levelsTests {
-		setBitWidth(e, bits.MaxLen8(bits.BytesToInt8(test)))
+		setBitWidth(e, bits.MaxLen8(unsafecast.BytesToInt8(test)))
 
 		t.Run("", func(t *testing.T) {
 			var err error
@@ -328,7 +329,7 @@ func testInt32Encoding(t *testing.T, e encoding.Encoding) {
 
 		t.Run("", func(t *testing.T) {
 			var err error
-			var input = bits.Int32ToBytes(test)
+			var input = unsafecast.Int32ToBytes(test)
 			buffer, err = e.EncodeInt32(buffer, input)
 			assertNoError(t, err)
 			values, err = e.DecodeInt32(values, buffer)
@@ -348,7 +349,7 @@ func testInt64Encoding(t *testing.T, e encoding.Encoding) {
 
 		t.Run("", func(t *testing.T) {
 			var err error
-			var input = bits.Int64ToBytes(test)
+			var input = unsafecast.Int64ToBytes(test)
 			buffer, err = e.EncodeInt64(buffer, input)
 			assertNoError(t, err)
 			values, err = e.DecodeInt64(values, buffer)
@@ -384,7 +385,7 @@ func testFloatEncoding(t *testing.T, e encoding.Encoding) {
 	for _, test := range floatTests {
 		t.Run("", func(t *testing.T) {
 			var err error
-			var input = bits.Float32ToBytes(test)
+			var input = unsafecast.Float32ToBytes(test)
 			buffer, err = e.EncodeFloat(buffer, input)
 			assertNoError(t, err)
 			values, err = e.DecodeFloat(values, buffer)
@@ -402,7 +403,7 @@ func testDoubleEncoding(t *testing.T, e encoding.Encoding) {
 	for _, test := range doubleTests {
 		t.Run("", func(t *testing.T) {
 			var err error
-			var input = bits.Float64ToBytes(test)
+			var input = unsafecast.Float64ToBytes(test)
 			buffer, err = e.EncodeDouble(buffer, input)
 			assertNoError(t, err)
 			values, err = e.DecodeDouble(values, buffer)
@@ -582,7 +583,7 @@ func benchmarkEncodeLevels(b *testing.B, e encoding.Encoding) {
 	testCanEncodeLevels(b, e)
 	buffer := make([]byte, 0)
 	values := generateLevelValues(benchmarkNumValues, newRand())
-	setBitWidth(e, bits.MaxLen8(bits.BytesToInt8(values)))
+	setBitWidth(e, bits.MaxLen8(unsafecast.BytesToInt8(values)))
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
 		benchmarkZeroAllocsPerRun(b, func() {
@@ -595,7 +596,7 @@ func benchmarkEncodeInt32(b *testing.B, e encoding.Encoding) {
 	testCanEncodeInt32(b, e)
 	buffer := make([]byte, 0)
 	values := generateInt32Values(benchmarkNumValues, newRand())
-	setBitWidth(e, bits.MaxLen32(bits.BytesToInt32(values)))
+	setBitWidth(e, bits.MaxLen32(unsafecast.BytesToInt32(values)))
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
 		benchmarkZeroAllocsPerRun(b, func() {
@@ -608,7 +609,7 @@ func benchmarkEncodeInt64(b *testing.B, e encoding.Encoding) {
 	testCanEncodeInt64(b, e)
 	buffer := make([]byte, 0)
 	values := generateInt64Values(benchmarkNumValues, newRand())
-	setBitWidth(e, bits.MaxLen64(bits.BytesToInt64(values)))
+	setBitWidth(e, bits.MaxLen64(unsafecast.BytesToInt64(values)))
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
 		benchmarkZeroAllocsPerRun(b, func() {
@@ -732,7 +733,7 @@ func benchmarkDecodeLevels(b *testing.B, e encoding.Encoding) {
 	testCanEncodeLevels(b, e)
 	values := generateLevelValues(benchmarkNumValues, newRand())
 	output := make([]byte, 0)
-	setBitWidth(e, bits.MaxLen8(bits.BytesToInt8(values)))
+	setBitWidth(e, bits.MaxLen8(unsafecast.BytesToInt8(values)))
 	buffer, _ := e.EncodeLevels(nil, values)
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
@@ -746,7 +747,7 @@ func benchmarkDecodeInt32(b *testing.B, e encoding.Encoding) {
 	testCanEncodeInt32(b, e)
 	values := generateInt32Values(benchmarkNumValues, newRand())
 	output := make([]byte, 0)
-	setBitWidth(e, bits.MaxLen32(bits.BytesToInt32(values)))
+	setBitWidth(e, bits.MaxLen32(unsafecast.BytesToInt32(values)))
 	buffer, _ := e.EncodeInt32(nil, values)
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
@@ -760,7 +761,7 @@ func benchmarkDecodeInt64(b *testing.B, e encoding.Encoding) {
 	testCanEncodeInt64(b, e)
 	values := generateInt64Values(benchmarkNumValues, newRand())
 	output := make([]byte, 0)
-	setBitWidth(e, bits.MaxLen64(bits.BytesToInt64(values)))
+	setBitWidth(e, bits.MaxLen64(unsafecast.BytesToInt64(values)))
 	buffer, _ := e.EncodeInt64(nil, values)
 
 	reportThroughput(b, benchmarkNumValues, len(values), func() {
diff --git a/internal/bits/max_test.go b/internal/bits/max_test.go
index f03c64a..a07100c 100644
--- a/internal/bits/max_test.go
+++ b/internal/bits/max_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/segmentio/parquet-go/internal/bits"
 	"github.com/segmentio/parquet-go/internal/quick"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func TestMaxBool(t *testing.T) {
@@ -174,7 +175,7 @@ func TestMaxFixedLenByteArray16(t *testing.T) {
 				}
 			}
 		}
-		ret := bits.MaxFixedLenByteArray(16, bits.Uint128ToBytes(values))
+		ret := bits.MaxFixedLenByteArray(16, unsafecast.Uint128ToBytes(values))
 		return (len(values) == 0 && ret == nil) || bytes.Equal(max[:], ret)
 	})
 	if err != nil {
diff --git a/internal/bits/min_test.go b/internal/bits/min_test.go
index 3daff50..9e99c6d 100644
--- a/internal/bits/min_test.go
+++ b/internal/bits/min_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/segmentio/parquet-go/internal/bits"
 	"github.com/segmentio/parquet-go/internal/quick"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func TestMinBool(t *testing.T) {
@@ -174,7 +175,7 @@ func TestMinFixedLenByteArray16(t *testing.T) {
 				}
 			}
 		}
-		ret := bits.MinFixedLenByteArray(16, bits.Uint128ToBytes(values))
+		ret := bits.MinFixedLenByteArray(16, unsafecast.Uint128ToBytes(values))
 		return (len(values) == 0 && ret == nil) || bytes.Equal(min[:], ret)
 	})
 	if err != nil {
diff --git a/internal/bits/minmax_test.go b/internal/bits/minmax_test.go
index c006e52..d275a90 100644
--- a/internal/bits/minmax_test.go
+++ b/internal/bits/minmax_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/segmentio/parquet-go/internal/bits"
 	"github.com/segmentio/parquet-go/internal/quick"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 const scaleFactor = 6000
@@ -226,7 +227,7 @@ func TestMinMaxFixedLenByteArray16(t *testing.T) {
 				}
 			}
 		}
-		minValue, maxValue := bits.MinMaxFixedLenByteArray(16, bits.Uint128ToBytes(values))
+		minValue, maxValue := bits.MinMaxFixedLenByteArray(16, unsafecast.Uint128ToBytes(values))
 		return (len(values) == 0 && minValue == nil && maxValue == nil) ||
 			(bytes.Equal(min[:], minValue) && bytes.Equal(max[:], maxValue))
 	})
diff --git a/internal/unsafecast/unsafecast_test.go b/internal/unsafecast/unsafecast_go18_test.go
similarity index 100%
rename from internal/unsafecast/unsafecast_test.go
rename to internal/unsafecast/unsafecast_go18_test.go
diff --git a/null_test.go b/null_test.go
index 462bb67..4a31ec0 100644
--- a/null_test.go
+++ b/null_test.go
@@ -47,7 +47,7 @@ func testNullIndex[T comparable](t *testing.T) {
 				}
 			}
 
-			array := makeArray(data)
+			array := makeArrayOf(data)
 			nullIndex[T](want, array, size, 0)
 			nullIndexFuncOf(reflect.TypeOf(zero))(got, array, size, 0)
 
@@ -88,7 +88,7 @@ func benchmarkNullIndex[T any](b *testing.B) {
 	typ := reflect.TypeOf(zero)
 	size := unsafe.Sizeof(zero)
 	null := nullIndexFuncOf(typ)
-	data := makeArray(make([]T, N))
+	data := makeArrayOf(make([]T, N))
 	bits := make([]uint64, (N+63)/64)
 
 	b.Run(typ.String(), func(b *testing.B) {
diff --git a/page_test.go b/page_test.go
index 4b97834..b199314 100644
--- a/page_test.go
+++ b/page_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/segmentio/parquet-go"
 	"github.com/segmentio/parquet-go/deprecated"
 	"github.com/segmentio/parquet-go/encoding/plain"
-	"github.com/segmentio/parquet-go/internal/bits"
+	"github.com/segmentio/parquet-go/internal/unsafecast"
 )
 
 func TestPage(t *testing.T) {
@@ -50,13 +50,13 @@ func testPageInt32(t *testing.T) {
 		testBufferPage(t, schema, pageTest{
 			write: func(w parquet.ValueWriter) (interface{}, error) {
 				values := []int32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
-				n, err := w.(io.Writer).Write(bits.Int32ToBytes(values))
+				n, err := w.(io.Writer).Write(unsafecast.Int32ToBytes(values))
 				return values[:n/4], err
 			},
 
 			read: func(r parquet.ValueReader) (interface{}, error) {
 				values := make([]int32, 10)
-				n, err := r.(io.Reader).Read(bits.Int32ToBytes(values))
+				n, err := r.(io.Reader).Read(unsafecast.Int32ToBytes(values))
 				return values[:n/4], err
 			},
 		})
@@ -86,13 +86,13 @@ func testPageInt64(t *testing.T) {
 		testBufferPage(t, schema, pageTest{
 			write: func(w parquet.ValueWriter) (interface{}, error) {
 				values := []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
-				n, err := w.(io.Writer).Write(bits.Int64ToBytes(values))
+				n, err := w.(io.Writer).Write(unsafecast.Int64ToBytes(values))
 				return values[:n/8], err
 			},
 
 			read: func(r parquet.ValueReader) (interface{}, error) {
 				values := make([]int64, 10)
-				n, err := r.(io.Reader).Read(bits.Int64ToBytes(values))
+				n, err := r.(io.Reader).Read(unsafecast.Int64ToBytes(values))
 				return values[:n/8], err
 			},
 		})
@@ -158,13 +158,13 @@ func testPageFloat(t *testing.T) {
 		testBufferPage(t, schema, pageTest{
 			write: func(w parquet.ValueWriter) (interface{}, error) {
 				values := []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
-				n, err := w.(io.Writer).Write(bits.Float32ToBytes(values))
+				n, err := w.(io.Writer).Write(unsafecast.Float32ToBytes(values))
 				return values[:n/4], err
 			},
 
 			read: func(r parquet.ValueReader) (interface{}, error) {
 				values := make([]float32, 10)
-				n, err := r.(io.Reader).Read(bits.Float32ToBytes(values))
+				n, err := r.(io.Reader).Read(unsafecast.Float32ToBytes(values))
 				return values[:n/4], err
 			},
 		})
@@ -194,13 +194,13 @@ func testPageDouble(t *testing.T) {
 		testBufferPage(t, schema, pageTest{
 			write: func(w parquet.ValueWriter) (interface{}, error) {
 				values := []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
-				n, err := w.(io.Writer).Write(bits.Float64ToBytes(values))
+				n, err := w.(io.Writer).Write(unsafecast.Float64ToBytes(values))
 				return values[:n/8], err
 			},
 
 			read: func(r parquet.ValueReader) (interface{}, error) {
 				values := make([]float64, 10)
-				n, err := r.(io.Reader).Read(bits.Float64ToBytes(values))
+				n, err := r.(io.Reader).Read(unsafecast.Float64ToBytes(values))
 				return values[:n/8], err
 			},
 		})