Yes, you must inherit from one of the types Ada.Finalization.Controlled
or Ada.Finalization.Limited_Controlled when you create a type for which
you can program an initializer and/or a finalizer.
However, you can aggregate a component of such a type into some other
composite type, and then that component's initializer and finalizer will
be called automatically when any object of the containing composite type
is constructed and destroyed.
> And you don't have automatic initialisation of
> subobjects and ancestors in a controlled order, nor automatic
> finalisation of them in the reverse order.
No, and yes. Subobjects (components) are automatically initialized
before the composite is initialized (bottom-up), and are automatically
finalized after the composite is finalized (top-down). But there is no
automatic invocation of the initializer or finalizer of the parent
class; that would have to be called explicitly (except in the case of an
"extension aggregate" expression, where an object of the parent type is
created and then extended to an object of the derived class).
The Ada initializer and finalizer concept is subtly different from the
C++ constructor and destructor concept. In Ada, the construction and
destruction are considered to happen implicitly and automatically. The
construction step can assign some initial values that can be defined by
default (pointers default to null, for example) or can be specified for
the type of the component in question, or can be defined for that
component explicitly. For example:
type Down_Counter is range 0 .. 100 with Default_Value => 100;
type Zero_Handler is access procedure;
type Counter is record
Count : Down_Counter; -- Implicit init to 100.
Running : Boolean := False; -- Explicit init.
At_Zero : Zero_Handler; -- Default init to null.
end record;
Beyond that automatic construction step, the programmable initializer is
used to perform further automatic activities that may further initialize
the object, or may have some other effects. For example, we might want
to automatically register every instance of a Counter (as above) with
the kernel, and that would be done in the initializer. Conversely, the
finalizer would then deregister the Counter, before the Counter is
automatically destroyed (removed from the stack or from the heap).
So the Ada "initializer" is not like a C++ constructor, which in Ada
corresponds more closely to a function returning an object of the class.
An Ada "finalizer" is more similar to a C++ destructor, taking care of
any clean-up that is needed before the object disappears.
>
> Let's take a little example. And since this is comp.arch.embedded,
> let's take a purely embedded example of disabling interrupts, rather
> than shunned dynamic memory allocations:
>
> static inline uint32_t disableGlobalInterrupts(void) {
> uint32_t pri;
> asm volatile(
> " mrs %[pri], primask\n\t" // Get old mask
> " cpsid i\n\t" // Disable interrupts entirely
> " dsb" // Ensures that this takes effect before next
> // instruction
> : [pri] "=r" (pri) :: "memory");
> return pri;
> }
>
> static inline void restoreGlobalInterrupts(uint32_t pri) {
> asm volatile(
> " msr primask, %[pri]" // Restore old mask
> :: [pri] "r" (pri) : "memory");
> }
I won't try to write Ada equivalents of the above :-) though I have of
course written much Ada code to manage and handle interrupts.
> class CriticalSectionLock {
> private :
> uint32_t oldpri;
> public :
> CriticalSectionLock() { oldpri = disableGlobalInterrupts(); }
> ~CriticalSectionLock() { restoreGlobalInterrupts(oldpri); }
> };
Here is the same in Ada. I chose to derive from Limited_Controlled
because that makes it illegal to assign a Critical_Section value from
one object to another.
-- Declaration of the type:
type Critical_Section is new Ada.Finalization.Limited_Controlled
with record
old_pri : Interfaces.Unsigned_32;
end record;
overriding procedure Initialize (This : in out Critical_Section);
overriding procedure Finalize (This : in out Critical_Section);
-- Implementation of the operations:
procedure Initialize (This : in out Critical_Section)
is begin
This.old_pri := disableGlobalInterrupts;
end Initialize;
procedure Finalize (This : in out Critical_Section)
is begin
restoreGlobalInterrupts (This.old_pri);
end Finalize;
>
> You can use it like this:
>
> bool compare_and_swap64(uint64_t * p, uint64_t old, uint64_t x)
> {
> CriticalSectionLock lock;
>
> if (*p != old) return false;
> *p = x;
> return true;
> }
>
function Compare_and_Swap64 (
p : access Interfaces.Unsigned_64;
old, x : in Interfaces.Unsigned_64)
return Boolean
is
Lock : Critical_Section;
begin
if p.all /= old then
return False;
else
p.all := x;
return True;
end if;
end Compare_and_Swap64;
(I think there should be a "volatile" spec for the "p" object, don't you?)
> This is the code compiled for a 32-bit Cortex-M device:
>
> <
https://godbolt.org/z/7KM9M6Kcd>
>
> The use of the class here has no overhead compared to manually disabling
> and re-enabling interrupts.
>
> What would be the Ada equivalent of this class, and of the
> "compare_and_swap64" function?
See above. I don't have an Ada-to-Cortex-M compiler at hand to compare
the target code, sorry.
But critical sections in Ada applications are more often written using
the Ada "protected object" feature. Here is the same as a protected
object "CS", with separate declaration and body as usual in Ada. Here I
must write the operation as a procedure instead of a function, because
protected objects have "single writer, multiple readers" semantics, and
any function is considered a "reader" although it may have side effects:
protected CS
with Priority => System.Interrupt_Priority'Last
is
procedure Compare_and_Swap64 (
p : access Interfaces.Unsigned_64;
old, x : in Interfaces.Unsigned_64;
result : out Boolean);
end CS;
protected body CS
is
procedure Compare_and_Swap64 (
p : access Interfaces.Unsigned_64;
old, x : in Interfaces.Unsigned_64;
result : out Boolean);
is begin
result := p.all = old;
if result then
p.all := x;
end if;
end Compare_and_Swap64;
end CS;
However, it would be more in the style of Ada to focus on the thing that
is being "compared and swapped", so that "p" would be either a
discriminant of the protected object, or a component of the protected
object, instead of a parameter to the copy-and-swap operation. But it
would look similar to the above.
>>> On the third hand (three hands are always useful for programming), the
>>> wordy nature of type conversions in Ada mean programmers would be
>>> tempted to take shortcuts and skip these extra types.
>>
>>
>> Huh? A normal conversion in C is written "(newtype)expression", the same
>> in Ada is written "newtype(expression)". Exactly the same number of
>> characters, only the placement of the () is different. The C form might
>> even require an extra set of parentheses around it, to demarcate the
>> expression to be converted from any containing expression.
>>
>> Of course, in C you have implicit conversions between all kinds of
>> numerical types, often leading to a whole lot of errors... not only
>> apples+oranges, but also truncation or other miscomputation.
>
> C also makes explicit conversions wordy, yes. In C++, you can choose
> which conversions are explicit and which are implicit - done carefully,
> your safe conversions will be implicit and unsafe ones need to be explicit.
Ada does not have programmable implicit conversions, but one can
override some innocuous operator, usually "+", to perform whatever
conversions one wants. For example:
function "+" (Item : Boolean) return Float
is (if Item then 1.0 else 0.0);
or more directly
function "+" (Item : Boolean) return Float
is (Float (Boolean'Pos (Item)));
> (C++ suffers from its C heritage and backwards compatibility, meaning it
> can't fix things that were always implicit conversion. It's too late to
> make "int x = 123.4;" an error. The best C++ can do is add a new syntax
> with better safety - so "int y { 123 };" is fine but "int z { 123.4 };"
> is an error.)
Ada also has some warts, but perhaps not as easily illustrated.