Oberon is dead, long live Oberon! Part 2. Modules
There are a lot of publications and discussions about the necessity / uselessness, advantages / disadvantages of the concept of modules in programming languages, so I’ll just talk about the implementation of the module system in the languages of the Oberon family.
A module in Oberons is not only a unit of compilation, loading and linking, it is also an encapsulation mechanism. When accessing entities of a connected (imported) module, mandatory qualification of this module is required. For example, if module A imports module B and uses its variable v , then the access to this variable should be of the form Bv, which reduces the number of hard-to-track errors of using completely different entities with the same name in non-modular languages, depending on the file connection sequence and compiler behavior.
As I already said, encapsulation in Oberons is also built on the concept of the module - all types declared in the module are transparent to each other, and external clients access the module entities through access specifiers. Currently, Active Oberon has the following access specifiers:
* “full access” specifier - the identifier is marked with an asterisk (*);
* qualifier "read-only access" - the identifier is marked with a minus sign (-);
Identifiers with missing qualifiers are not available to external clients.
For example:
Describes the record type Example1 exported outside the module. Field a is read and write to clients of the module in which the type is declared, field b is read-only and field c is hidden from external clients.
The name of the module (the resulting object file) specified after the MODULE keyword may not coincide with the file name, which is used, for example, to separate module implementations. This mechanism can be used instead of the conditional compilation directive mechanism - we always connect a module with a well-known and fixed name, and the build tools generate the necessary module in accordance with the assembly conditions - various OS, processors, etc.
In Active Oberon, plug-in (imported) modules can have aliases, different modules can have the same alias, forming a kind of namespace or pseudo-module, namespace entities can be accessed by its name, real module names are not available. Names of entities in such a namespace must not overlap.
Historically, the Oberon operating environment usually provides an interface for dynamically loading and unloading modules, although static linking is possible, as in Pow! or OO2C. The language itself provides only a module import section and a module initialization section. In some implementations, there is also a module finalization section, but, in general, the runtime environment provides the programmer with an interface for registering finalizer procedures that are automatically called when the module is unloaded or when the program terminates during static binding.
Typical module structure in Active Oberon:
(Yes, in Active Oberon the keywords can be in lower case, and not just CAPS, the selection of a set is carried out according to the form of writing the first significant identifier in the module - the MODULE keyword. If it is written in lower case, then all the keywords of the module must also be in lower case, if - MODULE, then in upper
case .) If the module is loaded dynamically, then you can unload it only if it is not referenced from the import section of other modules, i.e. unloading should be carried out in reverse order. Unloading modules in OS A2 is performed by the SystemTools.Free commands, which accepts a list of unloaded modules and SystemTools.FreeDownTo, which accepts a list of modules that should be unloaded after unloading all (recursively) referencing them.
The operating environment tries not to unload the modules, allowing the programmer to choose the unloading time (including all depending on what is required), since the dynamic loading has significant disadvantages besides the pluses - with an inaccurate approach to unloading, you can get a situation when there are no links to the module from the import sections and it is unloaded by the user, and the callbacks set by him, for example, remain, which will cause an exceptional situation. Because, as the Little Prince said, “there is such a firm rule - got up in the morning, washed, tidied himself up - and put your planet in order right away”. In other words, if we instructed the callbacks, then we should remove them, which is done in the finalizers of the module. It’s good practice to create default default implementations for procedural variables - stubs,
It is more difficult to deal with instances of reference types because the link may be in a module that did not connect the module in which the type is implemented, and formally there are no links in the import sections. In part, this can be fought using factories.
When the module is unloaded, the code and data are unloaded, with the exception of type descriptors, since instances of these types may remain in the system. All pointers in type descriptors, including method pointers in VMT, are set to NIL. When accessing such entities, an exceptional situation will occur.
As you can see, the ascetic implementation of the Oberon-System has both pros and cons that should be taken into account in their development. There are no real problems to eliminate these shortcomings, except for complicating the runtime environment and the compiler.
It is possible that with the help of the community, these problems can be solved by putting Oberon into a new orbit.
A module in Oberons is not only a unit of compilation, loading and linking, it is also an encapsulation mechanism. When accessing entities of a connected (imported) module, mandatory qualification of this module is required. For example, if module A imports module B and uses its variable v , then the access to this variable should be of the form Bv, which reduces the number of hard-to-track errors of using completely different entities with the same name in non-modular languages, depending on the file connection sequence and compiler behavior.
As I already said, encapsulation in Oberons is also built on the concept of the module - all types declared in the module are transparent to each other, and external clients access the module entities through access specifiers. Currently, Active Oberon has the following access specifiers:
* “full access” specifier - the identifier is marked with an asterisk (*);
* qualifier "read-only access" - the identifier is marked with a minus sign (-);
Identifiers with missing qualifiers are not available to external clients.
For example:
TYPE
Example* = RECORD a*, b-, c : LONGINT; END;
Describes the record type Example1 exported outside the module. Field a is read and write to clients of the module in which the type is declared, field b is read-only and field c is hidden from external clients.
The name of the module (the resulting object file) specified after the MODULE keyword may not coincide with the file name, which is used, for example, to separate module implementations. This mechanism can be used instead of the conditional compilation directive mechanism - we always connect a module with a well-known and fixed name, and the build tools generate the necessary module in accordance with the assembly conditions - various OS, processors, etc.
In Active Oberon, plug-in (imported) modules can have aliases, different modules can have the same alias, forming a kind of namespace or pseudo-module, namespace entities can be accessed by its name, real module names are not available. Names of entities in such a namespace must not overlap.
Historically, the Oberon operating environment usually provides an interface for dynamically loading and unloading modules, although static linking is possible, as in Pow! or OO2C. The language itself provides only a module import section and a module initialization section. In some implementations, there is also a module finalization section, but, in general, the runtime environment provides the programmer with an interface for registering finalizer procedures that are automatically called when the module is unloaded or when the program terminates during static binding.
Typical module structure in Active Oberon:
module Name;
import Modules, ....;
procedure Finalize*;
begin
...
end Finalize;
begin (* инициализация модуля *)
Modules.InstallTermHandler(Finalize); (* регистрация финализатора модуля *)
...
end Name.
(Yes, in Active Oberon the keywords can be in lower case, and not just CAPS, the selection of a set is carried out according to the form of writing the first significant identifier in the module - the MODULE keyword. If it is written in lower case, then all the keywords of the module must also be in lower case, if - MODULE, then in upper
case .) If the module is loaded dynamically, then you can unload it only if it is not referenced from the import section of other modules, i.e. unloading should be carried out in reverse order. Unloading modules in OS A2 is performed by the SystemTools.Free commands, which accepts a list of unloaded modules and SystemTools.FreeDownTo, which accepts a list of modules that should be unloaded after unloading all (recursively) referencing them.
The operating environment tries not to unload the modules, allowing the programmer to choose the unloading time (including all depending on what is required), since the dynamic loading has significant disadvantages besides the pluses - with an inaccurate approach to unloading, you can get a situation when there are no links to the module from the import sections and it is unloaded by the user, and the callbacks set by him, for example, remain, which will cause an exceptional situation. Because, as the Little Prince said, “there is such a firm rule - got up in the morning, washed, tidied himself up - and put your planet in order right away”. In other words, if we instructed the callbacks, then we should remove them, which is done in the finalizers of the module. It’s good practice to create default default implementations for procedural variables - stubs,
It is more difficult to deal with instances of reference types because the link may be in a module that did not connect the module in which the type is implemented, and formally there are no links in the import sections. In part, this can be fought using factories.
When the module is unloaded, the code and data are unloaded, with the exception of type descriptors, since instances of these types may remain in the system. All pointers in type descriptors, including method pointers in VMT, are set to NIL. When accessing such entities, an exceptional situation will occur.
As you can see, the ascetic implementation of the Oberon-System has both pros and cons that should be taken into account in their development. There are no real problems to eliminate these shortcomings, except for complicating the runtime environment and the compiler.
It is possible that with the help of the community, these problems can be solved by putting Oberon into a new orbit.