Документация Engee

Модули

Модули в Julia помогают организовывать код в целостные единицы. Они синтаксически разграничиваются внутри module NameOfModule ... end и имеют следующие характеристики.

  1. Модули являются отдельными пространствами имен, каждое из которых представляет новую глобальную область. Это полезно, поскольку позволяет использовать одно и то же имя для разных функций или глобальных переменных без возникновения конфликта, если они находятся в разных модулях.

  2. В модулях есть средства для детального управления пространством имен: каждый модуль определяет набор имен, которые он экспортирует с помощью export и помечает как public, и может импортировать имена из других модулей с помощью операторов using и import (они будут рассматриваться ниже).

  3. Модули могут быть предварительно скомпилированы для ускорения загрузки. Они также могут содержать код для инициализации во время выполнения.

Как правило, в больших пакетах Julia вы увидите код модуля, организованный в файлы, например

module SomeModule

# здесь обычно находятся операторы export, public, using, import, которые будут рассматриваться ниже.

include("file1.jl")
include("file2.jl")

end

Файлы и имена файлов в основном не имеют отношения к модулям. Модули связаны только с выражениями модулей. Для каждого модуля может существовать несколько файлов, а для каждого файла — несколько модулей. Функция include ведет себя так, как если бы содержимое файла исходного кода обрабатывалось в глобальной области содержащего модуля. В этой главе приводятся короткие и упрощенные примеры, поэтому функция include использоваться не будет.

Рекомендуется не делать отступы в теле модуля, так как это обычно приводит к отступам в целых файлах. Кроме того, принято использовать стиль UpperCamelCase для имен модулей (как и для типов) и форму множественного числа, если это применимо, особенно если модуль содержит идентификатор с аналогичным именем, что позволит избежать конфликтов на уровне имен. Примеры:

module FastThings

struct FastThing
    ...
end

end

Управление пространством имен

Управление пространством имен относится к возможностям языка, позволяющим сделать имена в модуле доступными в других модулях. Связанные концепции и функциональные возможности будут подробно рассматриваться далее.

Квалифицированные имена

Имена функций, переменных и типов в глобальной области, такие как sin, ARGS и UnitRange, всегда принадлежат модулю, называемому родительским модулем, который можно найти в интерактивном режиме с помощью функции parentmodule, например

julia> parentmodule(UnitRange)
Base

На эти имена можно ссылаться и вне родительского модуля, добавляя к ним префикс в виде модуля, например Base.UnitRange. Такое имя называется квалифицированным. Родительский модуль может быть доступен с помощью цепочки подмодулей типа Base.Math.sin, где Base.Math называется путем к модулю. Из-за синтаксической неоднозначности для квалификации имени, содержащего только символы, такие как оператор, требуется вставить двоеточие. Например, Base.:+. Для небольшого числа операторов дополнительно требуются круглые скобки. Например, Base.:(==).

Если имя является квалифицированным, оно всегда доступно, а если речь идет о функции, к имени могут быть добавлены методы с использованием квалифицированного имени в качестве имени функции.

В модуле имя переменной можно зарезервировать без присваивания, объявив его как global x. Это предотвращает конфликты имен для глобальных файлов, инициализируемых после загрузки. Синтаксис M.x = y не работает для присваивания глобального объекта в другом модуле. Присваивание глобального объекта всегда локально и выполняется в одном модуле.

Списки экспорта

Имена (относящиеся к функциям, типам, глобальным переменным и константам) можно добавить в список экспорта модуля с помощью оператора export: это символы, которые импортируются при использовании модуля (using). Обычно они находятся в верхней части определения модуля или вблизи нее, чтобы читатели исходного кода могли легко их находить, как показано в примере:

julia> module NiceStuff
       export nice, DOG
       struct Dog end      # Одинарный тип, не экспортируется
       const DOG = Dog()   # Именованный экземпляр, экспортируется
       nice(x) = "nice $x" # Функция, экспортируется
       end;

Но это всего лишь предложение по стилю — модуль может иметь несколько операторов export в произвольных местах.

Обычно экспортируются имена, которые являются частью API (интерфейса прикладного программирования). В приведенном выше коде список экспорта предлагает пользователям использовать nice и DOG. Однако, поскольку квалифицированные имена всегда делают идентификаторы доступными, это всего лишь вариант организации API: в отличие от других языков, в Julia нет средств, позволяющих действительно скрыть внутреннее содержимое модуля.

Кроме того, некоторые модули вообще не экспортируют имена. Обычно это происходит, если они используют в API общие слова, такие как derivative, что может привести к конфликтам со списками экспорта других модулей. Управление конфликтами имен будет рассматриваться далее.

Чтобы пометить имя как общедоступное, не экспортируя его в пространство имен тех, кто вызывает using NiceStuff, можно использовать public вместо export. При этом помечаются общедоступные имена как часть публичного API без последствий для пространства имен. Ключевое слово public доступно только в Julia 1.11 и более поздних версиях. Чтобы сохранить совместимость с Julia 1.10 и более ранними версиями, используйте макрос @compat из пакета Compat или конструкцию с учетом версии

VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public a, b, c"))

Отдельные операторы using и import

Для интерактивного применения наиболее распространенным способом загрузки модуля является использование оператора using ModuleName. Происходит загрузка кода, связанного с модулем ModuleName, и

  1. имя модуля

  2. и элементы списка экспорта добавляются в окружающее глобальное пространство имен.

Технически оператор using ModuleName означает, что модуль с именем ModuleName будет доступен для разрешения имен по мере необходимости. Когда встречается глобальная переменная, не имеющая определения в текущем модуле, система будет искать ее среди переменных, экспортируемых модулем ModuleName, и использовать ее, если она там будет найдена. Это означает, что все случаи использования этой глобальной переменной в текущем модуле будут разрешаться в пользу определения этой переменной в модуле ModuleName.

Для загрузки модуля из пакета можно использовать оператор using ModuleName. Для загрузки модуля из локально определенного модуля перед именем модуля нужно добавить точку, например using .ModuleName.

Продолжим наш пример:

julia> using .NiceStuff

загрузит приведенный выше код, делая доступными NiceStuff (имя модуля), DOG и nice. Dog отсутствует в списке экспорта, но к нему можно получить доступ, если имя квалифицировано с помощью пути к модулю (который здесь является просто именем модуля) как NiceStuff.Dog.

Важно отметить, что using ModuleName — это единственная форма, для которой списки экспорта имеют значение.

Напротив:

julia> import .NiceStuff

добавляет в область только имя модуля. Для доступа к его содержимому пользователи должны будут использовать NiceStuff.DOG, NiceStuff.Dog и NiceStuff.nice. Обычно import ModuleName используется в контекстах, когда пользователь хочет сохранить пространство имен чистым. Как мы увидим в следующем разделе, import .NiceStuff эквивалентен using .NiceStuff: NiceStuff.

Несколько операторов using и import одного типа можно объединить в выражении, разделенном запятыми, например:

julia> using LinearAlgebra, Random

Операторы using и import с определенными идентификаторами и добавление методов

Когда за оператором using ModuleName: или import ModuleName: следует список имен, разделенных запятыми, модуль загружается, но оператор добавляет в пространство имен только эти конкретные имена. Примеры:

julia> using .NiceStuff: nice, DOG

будет импортировать имена nice и DOG.

Важно отметить, что имя модуля NiceStuff будет отсутствовать в пространстве имен. Чтобы сделать его доступным, его необходимо перечислить явным образом, как показано далее.

julia> using .NiceStuff: nice, DOG, NiceStuff

Если два или более пакетов/модулей экспортируют имя, и это имя не относится к одному и тому же объекту в каждом пакете, а пакеты загружаются посредством using без явного списка имен, то ссылаться на это имя без уточнения будет ошибкой. Поэтому рекомендуется, чтобы код, предназначенный для дальнейшей совместимости с будущими версиями своих зависимостей и Julia, например код в выпущенных пакетах, указывал имена, которые он использует из каждого загруженного пакета, например using Foo: Foo, f, а не using Foo.

В Julia есть две формы для, казалось бы, одного и того же действия, потому что только оператор import ModuleName: f позволяет добавлять методы к f без пути к модулю. То есть следующий пример приведет к ошибке:

julia> using .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:1

Эта ошибка предотвращает случайное добавление методов к функциям в других модулях, которые только планировалось использовать.

Есть два способа решения этой проблемы. Всегда можно уточнить имена функций с помощью пути к модулю.

julia> using .NiceStuff

julia> struct Cat end

julia> NiceStuff.nice(::Cat) = "nice 😸"

Или можно импортировать с помощью import конкретное имя функции.

julia> import .NiceStuff: nice

julia> struct Mouse end

julia> nice(::Mouse) = "nice 🐭"
nice (generic function with 3 methods)

Их выбор зависит от стиля. Первая форма дает понять, что вы добавляете метод к функции в другом модуле (помните, что импорт и определение метода могут находиться в отдельных файлах), а вторая форма короче, что особенно удобно, если вы определяете несколько методов.

Как только переменная станет видимой при использовании оператора using или import, модуль не сможет создать собственную переменную с тем же именем. Импортированные переменные доступны только для чтения. Присваивание глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, в противном случае возникает ошибка.

Переименование с помощью ключевого слова as

Идентификатор, добавленный в область с помощью оператора import или using, можно переименовать, используя ключевое слово as. Это полезно для разрешения конфликтов имен, а также сокращения имен. Например, модуль Base экспортирует имя функции read, но пакет CSV.jl также предоставляет CSV.read. Если планируется вызывать операцию чтения CSV много раз, целесообразно отказаться от классификатора CSV.. Но тогда остается неясным, на что мы ссылаемся, — на Base.read или CSV.read.

julia> read;

julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main

Эту проблему можно решить путем переименования.

julia> import CSV: read as rd

Можно также переименовать сами импортированные пакеты.

import BenchmarkTools as BT

Ключевое слово as работает с оператором using только тогда, когда в область попадает один идентификатор. Например, using CSV: read as rd поддерживается, а using CSV as C нет, поскольку он работает со всеми экспортированными именами в CSV.

Смешивание нескольких выражений с операторами using и import

Когда используется несколько операторов using или import любой из вышеперечисленных форм, их действие соединяется в порядке их появления. Примеры:

julia> using .NiceStuff         # Экспортированные имена и имя модуля

julia> import .NiceStuff: nice  # Позволяет добавлять методы в неполные функции

Введет все экспортируемые имена NiceStuff и само имя модуля в область, а также позволит добавлять методы в nice, не указывая имя модуля в качестве префикса.

Устранение конфликтов имен

Рассмотрим ситуацию, когда два пакета (или более) экспортируют одно и то же имя, как в примере ниже.

julia> module A
       export f
       f() = 1
       end
A
julia> module B
       export f
       f() = 2
       end
B

Оператор using .A, .B работает, но при попытке вызывать функцию f выводится ошибка с подсказкой.

julia> using .A, .B

julia> f
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.

Здесь Julia не может определить, на какую функцию f вы ссылаетесь, поэтому сделать выбор придется именно вам. Обычно используются следующие решения.

  1. Продолжение с использованием квалифицированных имен, таких как A.f и B.f. В этом случае контекст будет понятен читателю кода, особенно если функция f просто совпадает, но имеет разное значение в разных пакетах. Например, degree по-разному используется в математике, естественных науках и повседневной жизни, и эти значения должны разделяться.

  2. Использование ключевого слова as для переименования одного идентификатора или обоих, например:

  julia> using .A: f as f

  julia> using .B: f as g

B.f будет доступен как g. Здесь предполагается, что до этого вы не использовали оператор using A, что привело бы к добавлению функции f в пространство имен.

  1. Когда рассматриваемые имена имеют общее значение, как правило, один модуль импортирует его из другого или имеет упрощенный, базовый пакет с единственной функцией определения интерфейса, подобного этому, который могут использовать другие пакеты. Обычно имена таких пакетов заканчиваются на ...Base (что не имеет ничего общего с модулем Base в Julia).

Порядок приоритета определений

В общем случае существует четыре вида связывающих определений:

  1. Определения, предоставленные посредством неявного импорта через using M

  2. Определения, предоставленные посредством явного импорта (например, using M: x, import M: x)

  3. Определения, неявно объявленные как глобальные (через global x без указания типа)

  4. Определения, явно объявленные с помощью синтаксиса определения (const, global x::T, struct и т. д.)

С синтаксической точки зрения мы разделяем их на три уровня приоритета (от самого слабого к самому сильному)

  1. Неявные импорты

  2. Неявные объявления

  3. Явные объявления и импорты

В общем случае мы разрешаем замену более слабых связей более сильными:

julia> module M1; const x = 1; export x; end
Main.M1

julia> using .M1

julia> x # Неявное импортирование из M1
1

julia> begin; f() = (global x; x = 1) end

julia> x # Неявное объявление
ОШИБКА: UndefVarError: `x` не определено в `Main`
Предложение: добавьте соответствующий импорт или присвоение. Эта глобальная переменная была объявлена, но не присвоена.

julia> const x = 2 # Явное объявление
2

Однако в пределах явного уровня приоритета замена синтаксически запрещена:

julia> module M1; const x = 1; export x; end
Main.M1

julia> import .M1: x

julia> const x = 2
ОШИБКА: нельзя объявить константу Main.x; она уже была объявлена как импорт
Stacktrace:
 [1] верхний уровень области видимости
   @ REPL[3]:1

или игнорируется:

julia> const y = 2
2

julia> import .M1: x as y
ПРЕДУПРЕЖДЕНИЕ: импорт M1.x в Main конфликтует с существующим идентификатором; игнорируется.

Решение неявной привязки зависит от набора всех модулей using, видимых в текущем возрасте мира. Более подробную информацию см. в главе руководства, посвященной возрасту мира.

### [Определения верхнего уровня по умолчанию и пустые модули](@id Default-top-level-definitions-and-bare-modules)

Модули автоматически содержат операторы `using Core`, `using Base` и определения функций [`eval`](@ref) и [`include`](@ref), которые вычисляют выражения или файлы в глобальной области данного модуля.

Если эти определения по умолчанию не нужны, модули можно определить с помощью ключевого слова [`baremodule`](@ref) (примечание: модуль `Core` по-прежнему импортируется). С точки зрения модуля `baremodule` стандартный модуль `module` выглядит следующим образом.

baremodule Mod

using Base

eval(x) = Core.eval(Mod, x) include(p) = Base.include(Mod, p)

…​

end

Если даже модуль `Core` не требуется, модуль, который ничего не импортирует и не определяет никаких имен, может быть определен с помощью `Module(:YourNameHere, false, false)`, а код в нем может быть обработан с помощью макроса [`@eval`](@ref) или функции [`Core.eval`](@ref):

jldoctest julia> arithmetic = Module(:arithmetic, false, false) Main.arithmetic

julia> @eval arithmetic add(x, y) =
(x, y) add (generic function with 1 method)

julia> arithmetic.add(12, 13) 25

### [Стандартные модули](@id Standard-modules)

Существует три важных стандартных модуля.

* [`Core`](@ref) contains all functionality "built into" the language.
* [`Base`](@ref) contains basic functionality that is useful in almost all cases.
* [`Main`](@ref) is the top-level module and the current module, when Julia is started.


[NOTE]
.Standard library modules
====
По умолчанию в состав Julia входит несколько модулей стандартной библиотеки. Они функционируют как обычные пакеты Julia, за исключением того, что их не нужно устанавливать явным образом. Например, если вам требуется выполнить модульное тестирование, можно загрузить стандартную библиотеку `Test` следующим образом:
====

julia using Test

## [Подмодули и относительные пути](@id Submodules-and-relative-paths)

Модули могут содержать *подмодули*, в которых вложен тот же синтаксис `module ... end`. Их можно использовать для введения отдельных пространств имен, что может быть полезно для организации сложных баз кода. Обратите внимание, что каждый `module` вводит собственную [область](@ref scope-of-variables), поэтому подмодули автоматически не наследуют имена от своего родительского модуля.

Рекомендуется, чтобы подмодули ссылались на другие модули внутри охватывающего родительского модуля (включая последний), используя *относительные квалификаторы модуля* в операторах `using` и `import`. Относительный квалификатор модуля начинается с точки (`.`), что соответствует текущему модулю, а каждый последующий символ `.` ведет на родительский модуль текущего модуля. За ним должны следовать модули, если необходимо, и в итоге — фактическое имя, к которому нужно получить доступ. Все элементы разделяются символом `.`.  Однако в особом случае ссылка на корень модуля может быть написана без `.`, что позволяет избежать необходимости подсчитывать глубину для достижения этого модуля.

Рассмотрим следующий пример, где подмодуль `SubA` определяет функцию, которая затем расширяется в своем одноуровневом модуле:

jldoctest module_manual julia> module ParentModule module SubA export add_D # Экспортируемый интерфейс const D = 3 add_D(x) = x + D end using .SubA # Привносит add_D в пространство имен export add_D # Экспортирует его из модуля ParentModule module SubB import ..SubA: add_D # Относительный путь для одноуровневого модуля # import ParentModule.SubA: add_D # в пакете, например, когда он загружается с помощью or import, это будет эквивалентно предыдущему импорту, но не в REPL struct Infinity end add_D(x::Infinity) = x end end;

Вы можете увидеть код в пакетах, который в аналогичной ситуации использует импорт без `.`:

jldoctest julia> import ParentModule.SubA: add_D ERROR: ArgumentError: Package ParentModule not found in current path.

Однако, поскольку это работает через [загрузку кода](@ref code-loading), оно работает только в том случае, если `ParentModule` находится в пакете в файле. Если `ParentModule` был определен в REPL, необходимо использовать относительные пути:

jldoctest module_manual julia> import .ParentModule.SubA: add_D

Обратите внимание, что при вычислении значений также важен порядок определений. Рассмотрим

julia module TestPackage

export x, y

x = 0

module Sub using ..TestPackage z = y # ERROR: UndefVarError: y not defined in Main end

y = 1

end

где модуль `Sub` пытается использовать переменную `TestPackage.y` до того, как она была определена, поэтому у него нет значения.

По аналогичным причинам невозможно использовать циклическое упорядочение.

julia module A

module B using ..C # ERROR: UndefVarError: C not defined in Main.A end

module C using ..B end

end

## [Инициализация и предварительная компиляция модуля](@id Module-initialization-and-precompilation)

Загрузка больших модулей может занимать несколько секунд, поскольку для выполнения всех операторов в модуле часто требуется скомпилировать значительный объем кода. Для сокращения этого времени Julia создает предварительно скомпилированные кэши модуля.

При загрузке модуля с помощью оператора `import` или `using` автоматически создаются и используются предварительно скомпилированные файлы модуля (иногда называемые «файлами кэша»).  Если файлов кэша еще нет, модуль будет скомпилирован и сохранен для повторного использования в будущем. Чтобы создать эти файлы, не загружая модуль, можно также вызвать [`Base.compilecache(Base.identify_package("modulename"))`](@ref) вручную. Результирующие файлы кэша будут сохранены во вложенной папке `compiled` в папке `DEPOT_PATH[1]`. Если в вашей системе ничего не изменилось, эти файлы кэша будут использоваться при загрузке модуля с помощью оператора `import` или `using`.

В предварительно скомпилированных файлах хранятся определения модулей, типов, методов и констант. В них также могут храниться специализации методов и созданный для них код, но обычно для этого разработчик должен явно добавить директивы [`precompile`](@ref) или выполнить рабочие нагрузки, вызывающие принудительную компиляцию во время сборки пакета.

Однако если вы обновите зависимости модуля или измените его исходный код, модуль будет автоматически перекомпилирован при использовании оператора `using` или `import`. Зависимостями являются импортируемые модули, сборка Julia, включаемые файлы или явные зависимости, объявленные с помощью функции [`include_dependency(path)`](@ref) в файлах модуля.

Для зависимостей файла, загружаемых с помощью `include`, изменение определяется путем анализа того, является ли неизменным размер файла (`fsize`) или его содержимое (сжатое в хэш). Для зависимостей файла, загружаемых с помощью `include_dependency`, изменение определяется путем анализа того, является ли время модификации (`mtime`) неизменным или равным времени модификации, усеченному до ближайшей секунды (для систем, которые не могут копировать mtime с точностью в долях секунды). Также учитывается, совпадает ли путь к файлу, выбранный логикой поиска в `require`, с путем, по которому был создан файл предварительной компиляции. Кроме того, принимается во внимание набор зависимостей, уже загруженных в текущий процесс. Эти модули не будут перекомпилированы, даже если их файлы изменятся или исчезнут, чтобы исключить возникновение несовместимости между работающей системой и кэшем предварительной компиляции. Наконец, принимаются во внимание изменения в любых [настройках времени компиляции](@ref preferences).

Если известно, что предварительная компиляция модуля *небезопасна* (например, по одной из причин, описанных ниже), необходимо поместить функцию `+++__precompile__+++(false)` в файл модуля (обычно в верхнюю часть). В результате функция `Base.compilecache` выдаст ошибку, а оператор `using` или `import` загрузит модуль непосредственно в текущий процесс и пропустит процедуры предварительной компиляции и кэширования. Это также предотвращает импорт модуля любым другим предварительно скомпилированным модулем.

Возможно, вам потребуется быть в курсе некоторых особенностей поведения, присущих созданию добавочных общих библиотек и требующих внимания при написании модуля. Например, внешнее состояние не сохраняется. Чтобы учесть этот момент, следует явным образом отделить шаги инициализации, которые должны осуществляться во *время выполнения*, от шагов, которые могут происходить во *время компиляции*. Для этого Julia позволяет определить функцию `+++__init__+++()` в модуле, которая выполняет любые шаги инициализации, которые должны происходить во время выполнения. Эта функция не будет вызываться во время компиляции (`--output-*`). По сути, можно предположить, что она будет выполнена ровно один раз за все время существования кода. Конечно, при необходимости вы можете вызвать ее вручную, но по умолчанию считается, что эта функция работает с состоянием вычислений для локального компьютера, которое не нужно — или даже не следует — фиксировать в скомпилированном образе. Функция будет вызвана после загрузки модуля в процесс, в том числе если он загружается в процесс добавочной компиляции (`--output-incremental=yes`), но не будет вызвана, если модуль загружается в процесс полной компиляции.

В частности, если вы определите функцию `function +++__init__+++()` в модуле, то Julia вызовет `+++__init__+++()` сразу *после* загрузки модуля (например, с помощью `import`, `using` или `require`) во время выполнения *впервые* (т. е. `+++__init__+++` вызывается только один раз и только после выполнения всех операторов в модуле). Поскольку она вызывается после полного импорта модуля, функции `+++__init__+++` любых подмодулей или других импортированных модулей вызываются *до* вызова `+++__init__+++` охватывающего модуля. Это также синхронизируется между потоками, так что код может безопасно полагаться на этот порядок эффектов, так что все `+++__init__+++` будут выполнены в порядке зависимостей до завершения результата `using`. Однако они могут выполняться одновременно с другими методами `+++__init__+++`, которые не являются зависимостями, поэтому будьте осторожны при доступе к любому общему состоянию вне текущего модуля и используйте блокировки, когда это необходимо.

Два типичных случая использования `+++__init__+++` — это вызов функций инициализации во время выполнения внешних библиотек C и инициализация глобальных констант, которые включают указатели, возвращаемые внешними библиотеками.  Например, предположим, что мы вызываем библиотеку C `libfoo`, которая требует вызова функции инициализации `foo_init()` во время выполнения. Предположим, что мы также хотим определить глобальную константу `foo_data_ptr`, которая хранит возвращаемое значение функции `void *foo_data()`, определенной `libfoo` — эта константа должна инициализироваться во время выполнения (а не во время компиляции), поскольку адрес указателя будет меняться от запуска к запуску.  Вы можете сделать это, определив в своем модуле следующую функцию `+++__init__+++`:

julia const foo_data_ptr = Ref{Ptr{Cvoid}}(0) function init() ccall:foo_init, :libfoo), Cvoid, ( foo_data_ptr[] = ccall:foo_data, :libfoo), Ptr{Cvoid}, ( nothing end `

Обратите внимание, что вполне возможно определить глобальный объект внутри функции типа __init__. Это одно из преимуществ использования динамического языка. Сделав объект константой в глобальной области, можно гарантировать, что тип известен компилятору, и позволить ему генерировать более оптимизированный код. Очевидно, что любые другие глобальные объекты в модуле, который зависит от foo_data_ptr, также должны быть инициализированы в функции __init__.

Константы, связанные с большинством объектов Julia, которые не создаются с помощью ключевого слова ccall, не нужно помещать в функцию __init__: их определения могут быть предварительно скомпилированы и загружены из образа кэшированного модуля. К ним относятся сложные выделяемые в куче объекты, такие как массивы. Однако любую подпрограмму, возвращающую необработанное значение указателя, следует вызывать во время выполнения, в противном случае предварительная компиляция выполнена не будет (объекты Ptr превратятся в нулевые указатели, если только они не скрыты внутри объекта isbits). Сюда входят возвращаемые значения функций Julia @cfunction и pointer.

При использовании предварительной компиляции важно четко понимать различие между этапом компиляции и этапом выполнения. В этом режиме часто гораздо более очевидно, что Julia является компилятором, который позволяет выполнять произвольный код Julia, а не отдельным интерпретатором, который также генерирует скомпилированный код.

Ниже приводятся другие известные потенциальные сценарии сбоя:

  1. Глобальные счетчики (например, для попытки уникальной идентификации объектов). Рассмотрим следующий фрагмент кода.

  mutable struct UniquedById
     myid::Int
     let counter = 0
         UniquedById() = new(counter += 1)
     end
  end

Хотя целью этого кода было присвоить каждому экземпляру уникальный идентификатор, значение счётчика записывается в конце компиляции. Все последующие использования этого инкрементально скомпилированного модуля будут начинаться с того же значения счётчика. Обратите внимание, что objectid (который работает путём хеширования указателя памяти) имеет схожие проблемы (см. примечания об использовании Dict ниже). Одним из вариантов является использование макроса для захвата @__MODULE__ и сохранения его отдельно с текущим значением counter, однако, возможно, лучше переписать код так, чтобы он не зависел от этого глобального состояния.

  1. Ассоциативные коллекции (такие как Dict и Set) необходимо перехешировать в __init__. (В будущем может быть предусмотрен механизм регистрации функции инициализации.)

  2. В зависимости от побочных эффектов времени компиляции, сохраняющихся во время загрузки. Примеры включают: изменение массивов или других переменных в других модулях Julia; поддержание дескрипторов для открытия файлов или устройств; хранение указателей на другие системные ресурсы (включая память);

  3. Создание случайных «копий» глобального состояния из другого модуля путём прямого обращения к нему, а не через путь поиска. Например, (в глобальной области видимости):

  #mystdout = Base.stdout #= будет работать некорректно, поскольку Base.stdout будет скопирован в этот модуль =#
  # Вместо этого используйте функции метода доступа:
  getstdout() = Base.stdout #= наилучший вариант =#
  # Или переместите присваивание в среду выполнения:
  __init__() = global mystdout = Base.stdout #= также работает =#

Для операций, которые можно выполнять при предварительной компиляции кода, установлено несколько дополнительных ограничений, призванных помочь пользователю избежать других ситуаций с неправильным поведением.

  1. Вызов функции eval для возникновения побочного эффекта в другом модуле. Это также приведет к выдаче предупреждения, если установлен флаг добавочной предварительной компиляции.

  2. Операторы global const из локальной области после запуска функции __init__() (см. описание проблемы № 12010 с планами на добавление соответствующей ошибки).

  3. Замена модуля является ошибкой времени выполнения при выполнении добавочной предварительной компиляции.

Ниже перечислено еще несколько моментов, на которые следует обратить внимание.

  1. Перезагрузка кода или аннулирование кэша не выполняются после внесения изменений в сами исходные файлы (в том числе с помощью Pkg.update), и очистка не производится после Pkg.rm.

  2. При предварительной компиляции игнорируется поведение совместного использования памяти массива с измененной формой (каждое представление получает свою копию).

  3. Ожидание неизменности файловой системы между временем компиляции и временем выполнения, например @__FILE__ или source_path() для поиска ресурсов во время выполнения, или макрос BinDeps @checked_lib. Иногда это неизбежно. Однако, когда это возможно, рекомендуется скопировать ресурсы в модуль во время компиляции, чтобы их не нужно было искать во время выполнения.

  4. На данный момент сериализатор не обрабатывает должным образом объекты и финализаторы WeakRef (это будет исправлено в ближайшем выпуске).

  5. Обычно лучше избегать записи ссылок на экземпляры внутренних объектов метаданных, таких как Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry и полей этих объектов, так как это может дезориентировать сериализатор и не привести к желаемому результату. Запись не обязательно будет считаться ошибкой, но вы просто должны быть готовы к тому, что система попытается скопировать одни из них и создать единственный уникальный экземпляр других.

Иногда во время разработки модуля целесообразно отключить добавочную предварительную компиляцию. Включать или отключать предварительную компиляцию модуля можно с помощью флага командной строки --compiled-modules={yes|no|existing}. Когда Julia запускается с установленным флагом --compiled-modules=no, сериализованные модули в кэше компиляции игнорируются при загрузке модулей и зависимостей модулей. В некоторых случаях может потребоваться загрузить существующие предварительно скомпилированные модули, но не создавать новые. Это можно сделать, запустив Julia с --compiled-modules=existing. Более детальный контроль обеспечивается параметром --pkgimages={yes|no|existing}, который действует только на хранение машинного кода во время предварительной компиляции. Функцию Base.compilecache по-прежнему можно вызывать вручную. Состояние этого флага командной строки передается скрипту Pkg.build для отключения автоматического запуска предварительной компиляции при установке, обновлении и явной сборке пакетов.

Некоторые сбои предварительной компиляции можно также отладить с помощью переменных среды. Если задать JULIA_VERBOSE_LINKING=true, это может помочь устранить сбои при компоновке общих библиотек скомпилированного машинного кода. В документации для разработчиков в руководстве Julia есть раздел, посвященный особенностям внутреннего устройства Julia, в главе «Образы пакетов», где можно найти более подробные сведения.