Version 1.1 of acs/ac-00299.txt

Unformatted version of acs/ac-00299.txt version 1.1
Other versions for file acs/ac-00299.txt

!standard 13.11(21.5/3)          18-01-11 AC95-00299/00
!class Amendment 18-01-11
!status received no action 18-01-11
!status received 17-10-14
!subject Maximum alignment for an allocator
!summary
!appendix

!topic Maximum alignment for an allocator
!reference Ada 2012 RM13.11
!from Victor Porton 17-10-14
!keywords allocator alignment
!discussion

The problem description:

It is impossible to properly implement an allocator through a C function
(such as raptor_alloc_memory() from Raptor C library) which allocates a struct
and returns the pointer to the allocated struct.

It is because RM13.11(21.5/3) "The Alignment parameter is a nonzero integral
multiple of D'Alignment..."

If it were "The Alignment parameter is equal to D'Alignment", then we would be
able just to check (in Allocate procedure implementation) that

pragma Assert(Dummy_Record'Alignment mod Alignment = 0);
-- where Dummy_Record is an arbitrary C-convention record
-- (as all C records have the same alignment reqs)

(but this is not enough in the modern version of the RM).

So Alignment parameter may be arbitrarily big (subject to
'Max_Alignment_For_Allocation) and the C function alignment may not conform
to it.

Proposed solutions:

We can add to System.Storage_Pools:

function Max_Alignment(Pool : Root_Storage_Pool)
  return Storage_Elements.Storage_Count is (0);

Then allocator for a type T with T'Alignment would ensure (and otherwise raise
Program_Error) that "Max_Alignment(T'Storage_Pool) mod T'Alignment = 0" and
request alignment which is a divisor of Max_Alignment(T'Storage_Pool).

The deficiency of the above solution is that it introduces a new primitive
operation which may possibly clash with user-defined functions.

An alternative solution:

Add 'Max_Alignment pool attribute and the following syntax for a pool
Pool:

for Pool'Max_Alignment use <constant>.

***************************************************************

From: Randy Brukardt
Sent: Friday, October 20, 2017  8:43 PM

...
> It is impossible to properly implement an allocator through a C 
> function (such as raptor_alloc_memory() from Raptor C
> library) which allocates a struct and returns the pointer to the 
> allocated struct.
> 
> It is because RM13.11(21.5/3) "The Alignment parameter is a nonzero 
> integral multiple of D'Alignment..."

This message happened to appear while the ARG was waiting for another
message during our recent meeting, and everyone (to a man) said that they were
confused. We didn't discuss it further (you won't find it in the minutes).
Having read the comp.lang.ada discussion, I think you needed to better explain
your use case.

Certainly, one can always get a larger alignment by allocating extra memory
and aligning the result. Dmitry explained that technique on comp.lang.ada.

But you said that won't work, "because the C library I am writing bindings for
may try to free an object allocated by me (or I my need to free an object
allocated by the library)."

That seems to me to be an exceedingly bad idea. You are guaranteed to run into
issues if you try to do that (Ada and C using different representations, etc.).
I've always kept all allocation/deallocation of my libraries on one side or the
other. And if it is the C library that is doing the allocation/deallocation,
I've just used subprogram calls for implementing that, never a Storage_Pool.

I don't think there ever was any intention that a storage pool would work
seamlessly with a C function; they're intended for allocating *Ada* objects.

Keep in mind that all C interfacing is by definition implementation-defined,
since it depends heavily on the C compiler. There's only so much portability
that can be achieved in such interfacing; it can never be 100%.

Moreover, one usually puts the Ada versions of things into the Ada thick
binding. The C thin binding by definition is never going to be very usable.
(It can't use exceptions, or default parameters, or dispatching, or stronger
typing than the C, and so on. We did all of those things in the think Claw
libraries.) From your questions, you seem to be trying to implement a single
level binding and make it very usable. That's a losing game; an Ada library
should look like it is designed for Ada, not for C. And if you want to stay
as close as possible to the original C, then just do that and don't try to
get clever.

BTW, to prevent Ada from allocating anything for a type, you can use
Storage_Size => 0. That would be my reecommendation in this case.

***************************************************************

From: Victor Porton
Sent: Saturday, October 21, 2017  12:32 PM

Suppose we have a type T with Convention=>C.

Let A be an access to T. I want "new T'(...)" with expected type A to call
C function such as raptor_alloc_memory() to allocate the appropriate amount
of memory and do initialization of T with this memory. (And similarly with
freeing allocated memory, which I omit for brevity.)

The address of the allocated memory should be the same (e.g. to facilitate
memory deallocation both through Ada and through C) as the address of the C
pointer returned by raptor_alloc_memory(). Allocating and freeing through
Ada and through C should be able to be arbitrarily intermixed.

Because it is a Convention=>C type, there should be no problem of interacting
with C in this case, dot.

You accused me of creating thin C library bindings. It is false: I do thick
bindings. In the code I am writing, the below "new" operator is to be used
in implementation not in interface, so there is nothing wrong with it.

I would define an Ada memory pool for this.

The only problem is that there is no prohibition for the Ada allocator to
request greater alignment than T'Alignment. In this case the allocator would
not be guaranteed to work correctly and this would lead to undefined behavior.

It is possible to do this without "new" operator, like this (extracted from
https://github.com/vporton/redland-bindings/blob/ada2012/ada/src/rdf-raptor-log.adb):

Size: constant size_t :=
  size_t((T'Max_Size_In_Storage_Elements * Storage_Unit + (char'Size-1)) / char'Size);
Result2: constant chars_ptr := raptor_alloc_memory(Size);
Result: constant A :=
  Locator_Handle(Locator_Conv.To_Access(Result2));

Then I do assignment to "Result.all" to initialize the allocated memory.

(here Locator_Conv converts between chars_ptr and Ada access, as defined in
https://github.com/vporton/redland-bindings/blob/ada2012/ada/src/rdf-auxiliary-convert_void.adb)

The above way should work (and I assume it's portable), but this is much less
convenient than just calling "new T'(...)" would be.

That it cannot be done through "new" is clearly a misfeature of Ada. It is a
misfeature, dot.

I propose to add the following construct:

Add 'Max_Alignment pool attribute and the following syntax for a pool
Pool:

for Pool'Max_Alignment use <constant>;

Then allocator for a type T with T'Alignment would ensure (and otherwise raise
Program_Error) that "T'Storage_Pool'Max_Alignment mod T'Alignment = 0" and
request alignment which is a divisor of Max_Alignment(T'Storage_Pool).

This can be used as

for Pool'Max_Alignment use T'Alignment;

Because T is the Convention=>C type we need to deal in the pool, this would
suffice for it to work right always.

***************************************************************

From: Randy Brukardt
Sent: Tuesday, October 24, 2017  6:48 PM

Sorry about the delay getting these messages posted. I've been fighting a high
fever and not working much. Anyway, when you post from an alternative address
like this one, you are certain to get the message delayed (all non-member mail
requires moderation). OTOH, your normal address is getting quarantined by our
spam filter for some reason that I need to investigate when I feel better; it
may be possible to eliminate that block (it might be quite old).

Anyway, on to the show...

...
> Suppose we have a type T with Convention=>C.
> 
> Let A be an access to T. I want "new T'(...)" with expected type A to 
> call C function such as raptor_alloc_memory() to allocate the 
> appropriate amount of memory and do initialization of T with this 
> memory. (And similarly with freeing allocated memory, which I omit for
> brevity.)
>
> The address of the allocated memory should be the same (e.g. 
> to facilitate memory deallocation both through Ada and through C) as 
> the address of the C pointer returned by raptor_alloc_memory(). 
> Allocating and freeing through Ada and through C should be able to be 
> arbitrarily intermixed.

This cannot work. As Dmitry noted, if one initializes objects on the Ada side
then finalization data may be set up; if you *don't* finalize the objects on
the Ada side, you'll leave that data pointing into the void and that will not
work. All Ada types nominally require finalization; not all actually require
any execution when finalized but you cannot easily tell what a particular
implementation will do. 

> Because it is a Convention=>C type, there should be no problem of 
> interacting with C in this case, dot.

I see your point, but this was never intended to be a use-case.
 
> You accused me of creating thin C library bindings. It is
> false: I do thick bindings.

They look like thin C bindings to me, because the fact that the underlying
library is in C is painfully obvious. Moreover, you're preventing Ada users
from using anything without C-interfacing in extensions of your library (as
the extensions have to be C-interfacing: no containers, streams, files, tasks,
etc. [note the explicit use of access types would work, but I consider that
evil - not shared by all].

> The only problem is that there is no prohibition for the Ada allocator 
> to request greater alignment than T'Alignment. In this case the 
> allocator would not be guaranteed to work correctly and this would 
> lead to undefined behavior.

This is certainly not the only problem; you have usability issues at best and
quite possibly serious potential of erroneous execution because of the
finalization issue.

My advice would be to write a true thick binding where almost nothing of the
C interface shows through. Preferably with no visible access types at all (that
provides the maximum flexibility for storage management on the Ada user -
access types have to be manually managed). [Again, I note that other people
may have a different definition of a thick binding.]

***************************************************************

From: Victor Porton
Sent: Wednesday, October 25, 2017  12:51 AM

> This cannot work. As Dmitry noted, if one initializes objects on the 
> Ada side then finalization data may be set up; if you *don't* finalize 
> the objects on the Ada side, you'll leave that data pointing into the 
> void and that will not work. All Ada types nominally require 
> finalization; not

Why not use Unchecked_Deallocation to free an object?

I don't see any trouble here.

Because it is a Convention=>C object, accordingly C rules (we are here to use
both Ada and C rules simultaneously), finalization is always empty.

> actually require any execution when finalized but you cannot easily 
> tell what a particular implementation will do.
> 
> > Because it is a Convention=>C type, there should be no problem of 
> > interacting with C in this case, dot.
> 
> I see your point, but this was never intended to be a use-case.

OK, we can make a new use case.

> > You accused me of creating thin C library bindings. It is
> > false: I do thick bindings.
> 
> They look like thin C bindings to me, because the fact that the 
> underlying library is in C is painfully obvious. Moreover, you're 
> preventing Ada users from using anything without C-interfacing in 
> extensions of your library (as the extensions have to be 
> C-interfacing: no containers, streams, files, tasks, etc. [note the 
> explicit use of access types would work, but I consider that evil - 
> not shared by all].
> 
> > The only problem is that there is no prohibition for the Ada 
> > allocator to request greater alignment than T'Alignment. In this 
> > case the allocator would not be guaranteed to work correctly and 
> > this would lead to undefined behavior.
> 
> This is certainly not the only problem; you have usability issues at 
> best and quite possibly serious potential of erroneous execution 
> because of the finalization issue.

I do not see finalization issue.

Finalization is guaranteed to be no-op by C standard. As we interface with C
and use Convention=>C, C standard applies!

***************************************************************


Questions? Ask the ACAA Technical Agent