Unsafe pointers¶
Тип UnsafePointer - это один из нескольких типов указателей, доступных в стандартной библиотеке для косвенной ссылки на места в памяти.
Вы можете использовать UnsafePointer для динамического выделения и освобождения памяти или для указания на память, выделенную каким-либо другим фрагментом кода. Вы можете использовать эти указатели для написания кода, который взаимодействует с низкоуровневыми интерфейсами, для взаимодействия с другими языками программирования или для создания структур данных, подобных массивам. Но, как следует из названия, они по своей сути небезопасны. Например, при использовании небезопасных указателей вы несете ответственность за то, чтобы память выделялась и освобождалась правильно.
В общем, вам следует отдавать предпочтение безопасным типам указателей, когда это возможно, оставляя UnsafePointer для тех случаев использования, когда другие типы указателей не работают. Сравнение типов указателей стандартных библиотек приведено во введении к указателям.
LegacyUnsafePointer Описанная здесь реализация
UnsafePointerзаменяет более раннюю версию на несколько иной API. Старая версия была переименована вLegacyUnsafePointerи будет признана устаревшей. На справочной страницеUnsafePointerкратко описаны различия между новой и старой версиями. Чтобы облегчить этот переход, библиотека в настоящее время поддерживает неявное преобразование междуLegacyUnsafePointerиUnsafePointer, так что вы можете передавать устаревший указатель на функциюUnsafePointerи наоборот. Более подробную информацию о переходе на новыйUnsafePointerсмотрите в руководстве по переходу, представленном в UnsafePointer v2.
Основы Unsafe указателей¶
UnsafePointer - это тип, который содержит адрес памяти. Вы можете сохранять и извлекать значения в этой памяти. Тип UnsafePointer является универсальным — он может указывать на любой тип значения, а тип значения указывается в качестве параметра. Значение, на которое указывает указатель, иногда называется точкой отсчета.
# Выделите память для хранения значения
var ptr = alloc[Int](1)
# Инициализируйте выделенную память
ptr.init_pointee_copy(100)

Рисунок 1. Указатель и наблюдатель¶
Обращение к памяти для извлечения или обновления значения называется разыменованием указателя. Вы можете разыменовать указатель, заключив после имени переменной пару пустых квадратных скобок:
# Update an initialized value
ptr[] += 10
# Access an initialized value
print(ptr[])
110
Вы также можете выделить память для хранения нескольких значений для создания структур, подобных массивам. Дополнительные сведения см. в разделе Хранение нескольких значений.
Жизненный цикл указателя¶
В любой момент времени указатель может находиться в одном из нескольких состояний:
-
Неинициализирована. Как и любая переменная, переменная типа
UnsafePointerможет быть объявлена, но неинициализирована.var ptr: UnsafePointer[Int, MutOrigin.external] -
Null. Нулевой указатель имеет адрес, равный 0, что указывает на недопустимый указатель.
ptr = {} -
Указывает на выделенную, неинициализированную память. Функция
alloc()возвращает указатель на вновь выделенный блок памяти, содержащий пространство для указанного количества элементов типа, указанного пользователем.Попытка разыменования указателя на неинициализированную память приводит к неопределенному поведению.ptr = alloc[Int](1) -
Указывает на инициализированную память. Вы можете инициализировать выделенный неинициализированный указатель, переместив или скопировав существующее значение в память. Или вы можете получить указатель на существующее значение, вызвав конструктор с аргументом ключевого слова
to.ptr.init_pointee_copy(value) # или ptr.init_pointee_move(value^) # или ptr = UnsafePointer(to=value)Как только значение инициализировано, вы можете прочитать или изменить его, используя синтаксис разыменования:
- Висячий(Dangling). Когда вы освобождаете выделенную указателю память, у вас остается висячий указатель. Адрес по-прежнему указывает на его предыдущее местоположение, но память для этого указателя больше не выделяется. Попытка разыменования указателя или вызова любого метода, который мог бы получить доступ к ячейке памяти, приводит к неопределенному поведению.var oldValue = ptr[] ptr[] = newValueptr.free()
На следующей диаграмме показан жизненный цикл UnsafePointer:

Рисунок 2. Жизненный цикл UnsafePointer¶
Выделение памяти¶
Используйте функцию alloc() для выделения памяти. Эта функция возвращает новый указатель, указывающий на запрашиваемую память. Вы можете выделить пространство для одного или нескольких значений типа указателя.
var ptr = alloc[Int](10) # Allocate space for 10 Int values
Выделенное пространство неинициализировано как переменная, которая была объявлена, но не инициализирована.
Инициализация наблюдателя(pointee)¶
Для инициализации выделенной памяти UnsafePointer предоставляет методы init_pointee_copy() и init_pointee_move(). Например:
ptr.init_pointee_copy(my_value)
Чтобы переместить значение в ячейку памяти указателя, используйте функцию init_pointee_move():
str_ptr.init_pointee_move(my_string^)
Обратите внимание, что для перемещения значения обычно требуется добавить символ передачи (^), если только значение не является тривиальным типом (например, Int) или недавно созданным "собственным" значением:
str_ptr.init_pointee_move("Owned string")
В качестве альтернативы вы можете получить указатель на существующее значение, вызвав конструктор UnsafePointer с ключевым словом to в качестве аргумента. Это полезно, например, для получения указателя на значение в стеке.
var counter: Int = 5
var ptr = UnsafePointer(to=counter)
Обратите внимание, что при вызове UnsafePointer(to=value) вам не нужно выделять память, поскольку вы указываете на существующее значение.
Разыменование указателей¶
Используйте оператор разыменования [] для доступа к значению, хранящемуся в указателе ("pointee").
# Read from pointee
print(ptr[])
# mutate pointee
ptr[] = 0
5
Если вы выделили пространство для нескольких значений, вы можете использовать синтаксис индекса для доступа к значениям, как если бы они были массивом, например, ptr[3]. Пустой индекс [] имеет то же значение, что и [0].
Внимание Оператор разыменования предполагает, что память, на которую выполняется разыменование, инициализирована. Разыменование неинициализированной памяти приводит к неопределенному поведению.
Вы не можете безопасно использовать оператор разыменования в неинициализированной памяти, даже для инициализации объекта-указателя. Это связано с тем, что присвоение разыменованному указателю вызывает методы жизненного цикла существующего объекта-указателя (такие как деструктор, конструктор перемещения или конструктор копирования).
var str_ptr = alloc[String](1)
# str_ptr[] = "Testing" # Undefined behavior!
str_ptr.init_pointee_move("Testing")
str_ptr[] += " pointers" # Works now
Уничтожение или удаление значений¶
Метод take_pointee() перемещает объект из ячейки памяти, на которую указывает ptr. Это требует больших затрат времени. Он вызывает __moveinit__() для целевого значения. При этом ячейка памяти остается неинициализированной.
Метод destroy_pointee() вызывает деструктор для объекта-получателя и оставляет область памяти, на которую указывает ptr, неинициализированной.
Как take_pointee(), так и destroy_pointee() требуют, чтобы указатель был ненулевым, а ячейка памяти содержала допустимое инициализированное значение типа объекта, на который указывает объект; в противном случае функция приводит к неопределенному поведению.
Вызов init_pointee_move_from(self, src) перемещает значение, на которое указывает src, в ячейку памяти, на которую указывает self. После этой операции право собственности на это значение переходит от src к self, а память в src неинициализируется: не считывайте из нее данные и не вызывайте для нее деструкторы. Чтобы снова сделать память действительной, инициализируйте ее новым значением, используя одну из операций init_pointee_*().
Mojo предполагает, что целевая память неинициализирована. Это не уничтожает существующее содержимое перед записью значения из
src.
Освобождение памяти¶
Вызов функции free() для указателя освобождает память, выделенную указателем. Он не вызывает деструкторы для каких—либо значений, хранящихся в памяти - вам нужно сделать это явно (например, с помощью destroy_pointee() или одной из других функций, описанных в разделе Уничтожение или удаление значений).
Удаление указателя без освобождения связанной с ним памяти может привести к утечке памяти, когда ваша программа продолжает занимать все больше и больше памяти, поскольку освобождается не вся выделенная память.
С другой стороны, если у вас есть несколько копий указателя, обращающихся к одной и той же памяти, вам нужно убедиться, что вы вызываете free() только для одной из них. Освобождение одной и той же памяти дважды также является ошибкой.
После освобождения памяти указателя вы остаетесь с зависшим указателем — его адрес по-прежнему указывает на освобожденную память. Любая попытка доступа к памяти, например, разыменование указателя, приводит к неопределенному поведению.
Хранение нескольких значений¶
Как упоминалось в разделе "Выделение памяти", вы можете использовать UnsafePointer для выделения памяти для нескольких значений. Память выделяется как единый непрерывный блок. Указатели поддерживают арифметику: добавление целого числа к указателю возвращает новый указатель, смещенный на указанное количество значений от исходного указателя:
var third_ptr = first_ptr + 2
Указатели также поддерживают вычитание, а также сложение и вычитание на месте:
# Advance the pointer one element:
ptr += 1

Рисунок 3. Арифметика указателей¶
Например, в следующем примере выделяется память для хранения 6 значений Float64 и все они инициализируются нулем.
var float_ptr = alloc[Float64](6)
for offset in range(6):
(float_ptr+offset).init_pointee_copy(0.0)
Как только значения будут инициализированы, вы сможете получить к ним доступ, используя синтаксис подстрочных индексов:
float_ptr[2] = 3.0
for offset in range(6):
print(float_ptr[offset], end=", ")
0.0, 0.0, 3.0, 0.0, 0.0, 0.0,
UnsafePointer и источники¶
Структура UnsafePointer имеет параметр origin, позволяющий отслеживать происхождение памяти, на которую он указывает.
Для указателей, инициализированных аргументом ключевого слова to, в качестве источника указывается источник объекта, на который он указывает. Например, в следующем коде s_ptr.origin совпадает с исходным кодом s:
s = "Testing"
s_ptr = UnsafePointer(to=s)
При выделении памяти с помощью функции alloc() возвращаемый указатель имеет исходное значение MutExternalOrigin. Это значение представляет исходное значение, которое может изменяться и не содержит псевдонимов существующих значений. Например, оно не указывает на память, выделенную для какой-либо другой переменной. Это означает, что компилятор не будет без необходимости продлевать время жизни несвязанных значений. Эта память не отслеживается программой Mojo, и вы несете ответственность за ее освобождение.
Если вы используете указатель в реализации структуры, вам обычно не нужно беспокоиться о происхождении, если указатель не отображается за пределами структуры. Например, если вы реализуете тип статического массива, который выделяет память в своем конструкторе, освобождает ее в своем деструкторе и не предоставляет указатель за пределы структуры, то исходный код по умолчанию будет в порядке.
Но если структура предоставляет указатель на эту память, вам нужно соответствующим образом задать источник. Например, тип List имеет метод unsafe_ptr(), который возвращает UnsafePointer в базовое хранилище. В этом случае возвращаемый указатель должен указывать на источник списка, поскольку список является логическим владельцем хранилища.
Этот метод выглядит примерно так:
fn unsafe_ptr(
ref self,
) -> UnsafePointer[T, origin_of(self)]:
return self.data.unsafe_origin_cast[origin_of(self)]()
Это возвращает копию исходного указателя, при этом исходное значение устанавливается в соответствии с исходным значением и возможностью изменения значения self.
Подобный метод небезопасен, но установка правильного исходного значения делает его более безопасным, поскольку компилятор знает, что указатель ссылается на данные, принадлежащие списку.
Работа с внешними указателями¶
При обмене данными с другими языками программирования вам может потребоваться создать UnsafePointer из внешнего указателя. Mojo ограничивает создание экземпляров UnsafePointer по произвольным адресам, чтобы пользователи случайно не создавали указатели, которые являются псевдонимами друг друга (то есть два указателя, которые ссылаются на одно и то же местоположение). Однако существуют специальные методы, которые вы можете использовать для получения небезопасного указателя из указателя Python или C/C++.
При работе с памятью, выделенной в другом месте, вам необходимо знать, кто отвечает за освобождение памяти. Освобождение памяти, выделенной в другом месте, может привести к неопределенному поведению.
При работе с некоторыми внешними функциями вам может потребоваться указать указатель без определенного типа (указатель со стиранием типа или "пустой указатель" в C/C++). Это эквивалентно Mojo OpaquePointer.
Вам также необходимо знать формат данных, хранящихся в памяти, включая типы данных и порядок байт.
Создание указателя Mojo на основе исходного адреса в памяти¶
Вы можете создать UnsafePointer из необработанного адреса в памяти, используя инициализатор unsafe_from_address.
fn write_to_address(mmio_address: Int, value: Int32):
var ptr = UnsafePointer[Int32, MutOrigin.external](
unsafe_from_address=mmio_address
)
# Запись в необработанный адрес памяти может потребовать энергозависимой загрузки/сохранения, поскольку операция
# может иметь побочные эффекты, невидимые компилятору.
# Вы можете указать это, используя параметр `volatile`.
ptr.store[volatile = True](value)
Это небезопасно, так как вызывающий объект должен убедиться, что адрес действителен перед записью в него, и что память инициализирована перед чтением из нее. Вызывающий объект также должен убедиться, что для адреса действителен источник и изменяемость указателя, невыполнение этого требования может привести к неопределенному поведению.
Создание указателя Mojo из указателя Python¶
Тип PythonObject определяет метод unsafe_get_as_pointer() для создания UnsafePointer из адреса Python.
Например, следующий код создает массив NumPy, а затем обращается к данным с помощью указателя Mojo:
from python import Python
def share_array():
np = Python.import_module("numpy")
arr = np.array(Python.list(1, 2, 3, 4, 5, 6, 7, 8, 9))
ptr = arr.ctypes.data.unsafe_get_as_pointer[DType.int64]()
for i in range(9):
print(ptr[i], end=", ")
print()
def main():
share_array()
1, 2, 3, 4, 5, 6, 7, 8, 9,
В этом примере используется атрибут NumPy ndarray.ctype для доступа к необработанному указателю на базовое хранилище (ndarray.ctype.data). Метод unsafe_get_as_pointer() создает UnsafePointer по этому адресу.
Работа с указателями на C/C++¶
Если вы вызываете функцию C/C++, которая возвращает указатель, используя функцию external_call, вы можете указать возвращаемый тип как UnsafePointer, и Mojo обработает преобразование типа за вас.
Примечательно, что параметр origin при работе за пределами границ FFI часто должен быть установлен в значение (Mut/Immut)ExternalOrigin, поскольку указатель указывает на память, внешнюю по отношению к программе mojo.
from sys.ffi import external_call
def get_foreign_pointer() -> UnsafePointer[Int, MutOrigin.external]:
var ptr = external_call[
"my_c_function", # external function name
UnsafePointer[Int, MutOrigin.external] # return type
]()
return ptr
Непрозрачные указатели¶
Тип OpaquePointer - это указатель, у которого нет определенного типа. В других языках это обычно называется указателем со стиранием типа или указателем void. Непрозрачные указатели обычно используются при взаимодействии с кодом, не относящимся к Mojo, таким как библиотечная функция C, которая принимает пустой указатель.
OpaquePointer на самом деле является псевдонимом типа для UnsafePointer[NoneType], поэтому он имеет тот же API, что и любой другой UnsafePointer.
Вы не можете разыменовать непрозрачный указатель, но вы можете привести его к определенному типу с помощью метода bitcast(). Аналогично, вы можете создать непрозрачный указатель из существующего указателя, преобразовав его в нетип. Например:
var str = "Hello, world!"
var str_ptr = UnsafePointer(to=str)
var opaque_ptr = str_ptr.bitcast[NoneType]()
# ... вызовите какую-нибудь внешнюю функцию, которая принимает указатель void
Преобразование данных: битовая трансляция и порядок байтов¶
Битовая трансляция указателя возвращает новый указатель, который имеет ту же ячейку памяти, но новый тип данных. Это может быть полезно, если вам нужно получить доступ к различным типам данных из одной области памяти. Это может произойти, когда вы читаете двоичные файлы, например файлы изображений, или получаете данные по сети.
В следующем примере обрабатывается формат, состоящий из блоков данных, где каждый блок содержит переменное количество 32-разрядных целых чисел. Каждый блок начинается с 8-разрядного целого числа, которое определяет количество значений в блоке.
def read_chunks(
var ptr: UnsafePointer[mut=False, UInt8],
) -> List[List[UInt32]]:
chunks = List[List[UInt32]]()
# Размер блока, равный 0, указывает на конец данных
chunk_size = Int(ptr[])
while (chunk_size > 0):
# Пропустите значение chunk_size размером в 1 байт и получите указатель на первый
# UInt32 в блоке
ui32_ptr = (ptr + 1).bitcast[UInt32]()
chunk = List[UInt32](capacity=chunk_size)
for i in range(chunk_size):
chunk.append(ui32_ptr[i])
chunks.append(chunk)
# Переместим наш указатель на следующий байт после текущего фрагмента
ptr += (1 + 4 * chunk_size)
# Считайте размер следующего фрагмента
chunk_size = Int(ptr[])
return chunks
Битовая трансляция указателя возвращает новый указатель, который имеет ту же ячейку памяти, но новый тип данных. Это может быть полезно, если вам нужно получить доступ к различным типам данных из одной области памяти. Это может произойти, когда вы читаете двоичные файлы, например файлы изображений, или получаете данные по сети.
В следующем примере обрабатывается формат, состоящий из блоков данных, где каждый блок содержит переменное количество 32-разрядных целых чисел. Каждый блок начинается с 8-разрядного целого числа, которое определяет количество значений в блоке.
chunk.append(byte_swap(ui32_ptr[i]))
Работа с SIMD-векторами¶
Тип UnsafePointer включает методы load() и store() для выполнения выровненных загрузок и сохранения скалярных значений. В нем также есть методы, поддерживающие пошаговую загрузку/сохранение и сбор/разброс.
Пошаговая загрузка загружает значения из памяти в вектор SIMD, используя смещение ("stride") между последовательными адресами памяти. Это может быть полезно для извлечения строк или столбцов из табличных данных или для извлечения отдельных значений из структурированных данных. Например, рассмотрим данные для изображения RGB, где каждый пиксель состоит из трех 8-битных значений - красного, зеленого и синего. Если вы хотите получить доступ только к значениям красного, вы можете использовать пошаговую загрузку или сохранение.

Рисунок 4. Ступенчатая нагрузка¶
Следующая функция использует методы strided_load() и strided_store() для инвертирования значений красных пикселей в изображении, по 8 значений за раз. (Обратите внимание, что эта функция обрабатывает только те изображения, где количество пикселей равномерно делится на восемь.)
def invert_red_channel(ptr: UnsafePointer[mut=True, UInt8], pixel_count: Int):
# количество значений, загруженных или сохраненных одновременно
comptime simd_width = 8
# байт на пиксель, что также является размером шага
bpp = 3
for i in range(0, pixel_count * bpp, simd_width * bpp):
red_values = ptr.offset(i).strided_load[width=simd_width](bpp)
# Инвертируйте значения и сохраняйте их в исходном расположении
ptr.offset(i).strided_store[width=simd_width](~red_values, bpp)
Методы gather() и scatter() позволяют загружать или сохранять набор значений, которые хранятся в произвольных местах. Для этого вы передаете SIMD-вектор смещений текущему указателю. Например, при использовании функции gather() n-е значение в векторе загружается из (адрес указателя) + offset[n].
Безопасность¶
Небезопасные указатели небезопасны по нескольким причинам:
-
Управление памятью зависит от пользователя. Вам необходимо вручную выделять и освобождать память и/или быть в курсе того, когда другие API-интерфейсы выделяют или освобождают память для вас.
-
Значения
UnsafePointerмогут иметь значениеnull, то есть указатель не обязательно будет указывать на что—либо. И даже если указатель указывает на выделенную память, эта память может не быть инициализирована. -
У
UnsafePointerесть параметрorigin, поэтому Mojo может отслеживать происхождение данных, на которые он указывает, но он также предоставляет небезопасные API. Например, когда вы выполняете арифметику с указателями, компилятор не выполняет никакой проверки границ.