How to get access to a record field

92 Views Asked by At

Some general context for my problem first.

I need to bind some arguments of a prepared statement using GNATCOLL SQLite bindings. These bindings expect C character pointer as an input (beside other things). This requirements creates two problems on Ada's end:

Problem 1

The variable pointed to by the "bound" pointer must not perish until the prepared statement is finalized (otherwise it will store a pointer to garbage). But, for queries that operate on the same type of record it would be desirable to extract the part of binding of arguments (which are obtained from the record fields) into a separate procedure. If such procedure returns before the statement is finalized the variables (on the stack of such procedure) will be deleted, and pointers now point to garbage.

Problem 2

I only know of three instances in Ada of creating pointers: new-ing, taking a pointer to a function / procedure and taking a pointer to a variable. Since the bindings want a pointer, I don't know how to extract such a pointer from a record unless I "manually unpack" it into a bunch of local variables. Needless to say this is ugly, repetitive and very prone to copy-and-paste error. This also leads to the lifetime issues (since variables "unpacked" in such a way will be deleted before the actual value they are used to capture still exists.)

Example

type Tag is record
  Field : String := "value";
end record;

type Tag_Access is access all Tag;

procedure Bind_Tag (T : Tag_Access; Stmt : Gnade.Statement) is
  --  This variable will vanish before the statement is executed
  Field : aliased constant String := T.Field;
begin
    Gnade.Bind_Text (Stmt, Field'Address, Field'Length);
end Bind_Tag;

procedure Insert_Tag (T : Tag) is
  --  Necessary connection initialization and building of prepared statement
  Tc : Tag := T;  --  Creating a useless variable only to please the compiler
  Ta : Tag_Access := Tc'Access;
begin
  Bind_Tag (Ta, Stmt);
  --  Here, bindings are dead, so we are executing garbage
  Gnade.Step (Db, Stmt);
end Insert_Tag;

If I may enter a plea

I suspect this may be helped by using objects (i.e. new-ing something). I haven't researched this approach because my first experience with Ada (I'm still learning) was very negative when contracting objects. Deallocation combined with absence of convenient object lifetime management (eg. equivalent of C++ RAII) makes using objects a very daunting task. I would like to stay away from this functionality as much as possible.


Edit

I found a way out of this particular conundrum: turns out SQlite can be instructed to make copies when binding strings. This isn't ideal, but at least I can get the strings into the database.

This doesn't mean that the question is solved though. I'd still like to know a more general way of dealing with record fields. Something that in eg. C would be accomplished by taking a pointer to the struct and then adding the size of the fields preceding the field of interest and adding that the the pointer.

1

There are 1 best solutions below

3
Jere On BEST ANSWER

In general to get an access to the field you should be doing a couple of things:

  1. Field in Tag should be marked as aliased. This tells the compiler you may plan on taking an access to it if needed.
  2. Your Tag parameter of Insert_Tag may also need to be aliased so that the compiler knows it is intended that the parameter be passed by reference. The compiler isn't supposed to allow you to take an access of a field (aliased or not) of an non aliased variable as the general case is not safe. You may need to propagate up this aliased property up the call chain to where the tag is declared and ensure that it is also declared as aliased. However, you still run into the problem that Tag_Access is library level and your object is not, so you will want to use the Unchecked_Access attribute instead of the Access attribute. However doing so means you take responsibility to ensure the lifetime of the variable isn't violated. The aliased marking on your parameters will help to solidify that. Alternately you can change Bind_Tag to take in an anonymous access type and then you can use the regular access attribute. Or even better, just use "aliased in out Tag" instead of the access type.
  3. You only need the dummy variable Tc because you declared Insert_Tag to take in a constant view of the Tag parameter. Making the parameter aliased in out would allow you avoid the dummy variable in that scenario.
  4. Ada does provide some support for RAII types. Ada.Containers.Indefinite_Holders which are used for single objects and the other containers which are used for collections of objects. You shouldn't need to do new or deallocation in a lot of circumstances. I agree it doesn't cover everything, but for starting out with the types you have shown in your example above, they should be able to handle allocation/deallocation of objects for you. If you have some examples that were tough, maybe we can start up another question and discuss it and see if we can find you a solution.

I've simplified some of your code based on the stuff mentioned above:

type Tag is record
  Field : aliased String(1..5) := "value";
end record;

type Tag_Access is access all Tag;

package Gnade is
    type Statement is null record;
    procedure Bind_Text(Stmt : Statement; Address : System.Address; Length : Natural) is null;
end Gnade;

procedure Bind_Tag (T : aliased in out Tag; Stmt : Gnade.Statement) is
begin
    Gnade.Bind_Text (Stmt, T.Field'Address, T.Field'Length);
end Bind_Tag;

procedure Insert_Tag (T : aliased in out Tag; Stmt : Gnade.Statement) is
begin
  Bind_Tag (T, Stmt);
  --  Here, bindings are dead, so we are executing garbage
  --Gnade.Step (Db, Stmt);
end Insert_Tag;

Statement : constant Gnade.Statement := (null record);
Value : aliased Tag;

This code compiles with a couple of online Ada compilers I tried.