Using access types with arrays as parameters to a subprogram

Hello,

I am trying to create a procedure that can operate on an access type of an array, in which the bounds shall be known at runtime.

In my code I am getting an error: “Object subtype must statically match designated subtype”.

  with Ada.Text_IO; use Ada.Text_IO;                                                                                                                                                                  
                                                                                                                                                                                                  
  procedure Example is                                                                                                                                                                                
     type u8 is mod 2 ** 8;                                                                                                                                                                           
     type Pixel is record                                                                                                                                                                         
       R : u8;                                                                                                                                                                                   
       G : u8;                                                                                                                                                                                   
       B : u8;                                                                                                                                                                                   
     end record;                                                                                                                                                                                  
     type Surface_Type is array (Natural range <>) of Pixel;                                                                                                                        
     type Surface_Access is access all Surface_Type;   
      
     procedure Surface_Updater (Surface : Surface_Access)                                                                                                                                                                   
     is                                                                                                                                                                                                   
     begin                                                                                                                                                                                                                  
       for P in Surface'Range loop
          -- Set pixel
          null;
       end loop;                                                                                                                                                                                                               
     end;                                                                                                                                                                                                                   
     Primary : aliased Surface_Type (1 .. 1024 * 768);                                                                                                                                               
  begin   
     Surface_Updater (Primary'Access);                                                                                                                                                                                      
  end Example;                                                                                                                                                                                                              

I found that I could write an operationally equivalent procedure by passing System.Address and length parameters instead of an access type, but I am curious if I can use an access type and loop over the referenced array.

Thanks,
N

Why do you need an access-to-array?
What is the problem with using procedure Surface_Updater (Surface : Surface_Type);? Are you using things direct, or interfacing to something else like (e.g.) OpenGL?

For access types, you have to discriminate between the nominal type and the actual type.
Surface_Type is unconstrained.
X: Surface_Type := Init;
is constrained by the initial value. The nominal type Surface_Type is unconstrained, the actual one constrained.
Y: Surface_Type (A .. B);
is constrained because already its nominal type Surface_Type (A .. B) is constrained.
So Surface_Access cannot point to Y, this is what the error message tries to explain.

Reason is: Objects with constrained nominal type do not store bounds, those with unconstrained nominal type do. The pointer has to know which kind of object it tries to grant access to.

2 Likes

I have a task which is interacting with a video 4 linux driver which is capturing the pixel data with the goal of getting the said data to an SDL buffer running on the primary subprogram (main task).

Ok, so, I think what I would recommend is System.Address_to_Access_Conversions (link), but also doing this inside the body, converting everything (as-needed) to/from Ada types.

I had started a conversion/interfacing layer for interfacing with some scientific cameras a couple years ago, and you can do a LOT by setting up your main program in terms of using Ada’s type-system to model the problem, and hiding away the messy “the API is defined in C!” stuff. (The particular solution I had was kinda like a cross between layering and Model-View-Controller.) — I highly recommend you have you main program interface/operate on a high-level, only going to low[er]-levels on successive layers.

Conceptual example:

  • Define “video-sequence” / use non-constrained form
  • Define Camera
  • use “resolution” from Camera to set constraints/parameters on sequence.
  • Define Camera.API; use subtype to constrain C-isms to actual Ada.
    • Function Init( Object : in out System.Address ) return C.Int with Post Init'Result in A_OK | Not_Found | Undefined_Error;
    • Use the above to map into real types/subtypes, presenting that to the next layer.
  • Tie together the low-level and high-level.

Which I hope is clear enough conceptually; basically you’re dealing with Ada type-objects, no bit-twiddling, no conversions for your main processing; as you go deeper to HW/C-API you start catering to that, though retaining a lot of the Ada, and then at the very end you interface directly (thin-API) — what this does is allows you to successively use the language to both catch more and more errors, as well as prevent errors in the first place — i.e. if you have an enumeration for resolution (CGA, EGA, VGA), you can use those to represent the proper values and not have to worry about:

  -- Low level
  Procedure Get_Data( Chunk : out Buffer; H,W : in C.Int )
    with Pre => ((H in 320|640|whatever) and
                ( case H is
                  when 320 => W = 480, --…
                )) or else raise API_Error with "Invalid resolution:"& 
                H'Image &" x" & W'Image;

by

Generic
  Size : Resolution;
Package Video_Buffer is
  Subtype Buffer_Type is Frame(
     (case Size is --… ),
     (case Size is --… )
   ) ;
   Function  Read return Buffer_Type;
   Procedure Write( Value : Buffer_Type );
-- …
end Video_Buffer;
Package body Video_Buffer is
  -- addr to acces conv. here.
   Function  Read return Buffer_Type is
   begin
      Return Result : aliased Buffer_Type do
         Get_Data( Result, result'last(1), Result'last(2) );
      End return;
   end Read;
end;

As you can see in the above, you’re eliminating what could go wrong.

Thanks for the suggestion. Instead of passing an access type (which I don’t know how to do), I am passing the address and length. Then in the task I create the same data type using the address passed.

The API I came up with is this:

package body Camera is
procedure Open 
   (Surface : System.Address;
    Width : Positive;
    Height : Positive) is
begin
   Surface_Height := Height; -- Surface_Height is private in Camera spec
   Surface_Width := Width; -- Surface_Width is private in Camera spec
   Surface_Address := Surface; -- Surface_Address is private in Camera spec
   Reader.Start;
end Open;

task body Reader is
  select
     accept Start do
        null; -- Syncrhonize with 'Open' operation
     end Start
   or
     terminate;
  end select;

  declare
     Surface : Surface_Type (1 .. Surface_Height * Surface_Width)
               with Address => Surface_Address;
  begin
     loop
        -- Write to Surface here with frame data
        select 
           accept Frame_Ready do
               null; -- Synchronize with SDL draw loop in main subprogram
           end Frame_Ready
        or
           terminate
        end select;
     end loop;
  end;
end Reader;
end Camera;

As a design principle consider this: a producer is never blocked by a consumer. Your design violates the principle because the frame reader is blocked until the frame is accepted. It should drop frames instead. And of course, you need no addresses in any case.

Normally one would use double or triple frame buffering. The reader would fill the oldest frame. The consumer would access the newest one. A protected object wrapping the frame buffers would do interlocking just in case.

Frame reader would either see the protected object declared in an outer scope or accept an access or handle/holder to it.

I think I may have miscommunicated.
Let me ask a different way:

  1. What data-types are best for your actual operation/process/“main-program”?
    1. How are they going to be used?
    2. What are the design-considerations?
    3. What properties & limitations do you want? and which are undesirable?
  2. What subprogram-interfaces are needed for binding the hardware? [Low-level]
    1. What types are “native” to this binding?
    2. What subtypes, if any, will catch or prevent errors?
    3. Note what has to be done to correctly interface. (e.g. In OpenGL, certain things cannot correctly be called without being after a glBegin and before a glEnd.)
  3. Given the constraints considered in #2.3, ask how you can enforce these. [Mid-level]
    (In OpenGL it could be that you have a global variable flagged for if you are between glBegin and glEnd.)
    1. In this level, you marry the native-/lowlevel to things that cannot error out (like enumerations with specific representations, that can be [e.g.] Unchecked_Conversion-translated directly to Interfaces.C.Int.)
    2. You are also enforcing (inasmuch as possible) the correct operation of the low-level interface.
    3. You are presenting “to the outside world” of your program, idiomatic/near-idiomatic Ada; though not necessarily “idiomatic Ada” WRT design.
  4. If needed, and taking into consideration #1, you would adapt #3 to #1; a “thick binding”, which is idiomatic Ada in design as well as in structure.

In terms of “main-program”, consider model-view-controller architecture, and how you could make it “double-sided” in a program:

  Display  <— Web-browser, or desktop GUI, or text.
  \     /  <— Translation Layer to/from display objects.
  (PROG.)  <— Actual processing/program; your "native Ada" datatypes.
  /     \  <— Translation Layer to/from database objects.
  Database <— Actual DB, file, IO/stream, whatever.

In the above, you’re applying interfaces to the outside world, whether that’s disk or web, that translate from those to your “more Ada-like” forms, and then from those “more Ada-like forms” to your actual “this is how the problem is modeled” datatypes. — It’s the same thing that you should do for binding up a an API, although I may be assuming incorrectly, and my previous example may have started at a bit too low-level.

Of course you can directly interface [thin-binding] but typically you’re not using Ada’s safety/correctness features if you do that. (Which could be legitimate, as in I’m just playing around, learning X.)

I hope that’s clear[er].

  1. Data Types: I have a SDL surface which is an array of RBGA pixels which should contain the latest data from the Camera sensor.
    1-1. Used: The Camera task writes to this surface and the surface gets copied into the SDL suface buffer inside the SDL redraw Lock()…Unlock() loop.
    1-2. Get it working then performance optimize later.
  2. Low level, there are bindings to C ioctl and C mmap. The data path for the renderer is:
    mmap (during initialization)
    [ c_select → pointer to pixel data + length → transform to RGB surface → copy to SDL texture inside Lock()…Unlock() ]
    I use a C pointer to overlay an Ada array of the Camera pixel type on top of the memory address then perform the color transform.

This looks like:

type YUY2_Macropixel is record
  Y0 : u8;
  U0 : u8;
  Y1 : u8;
  V0 : u8;
end record;
for YUY2_Macropixel use record
  Y0 at 0 range 0 .. 7;
  U0 at 0 range 8 .. 15;
  Y1 at 0 range 16 .. 23;
  V0 at 0 range 24 .. 31;
end record;
for YUY2_Macropixel'Size use 32;

YUY2 : YUY2_Macropixel_Array (1 .. Natural (mmap_Buffer_Length / 4)) with Address => mmap_Buffer_Address;

2-1 This is a V4L interface so it’s mostly bitfields, pointers, and lengths.
2-2. I don’t know.
2-3. This is similar to the SDL Lock()…Unlock() that I mentioned earlier. Guessing that the Texture reference is only valid inside these calls, which is why I have a buffer of the surface that get’s written to by the camera thread.

  1. I’m using rendezvous to synchronize the two tasks. Perhaps this is not the best method, but it works. As Dmitry pointed out I should use multiple buffers for performance optimization.

Ok, there’s a fundamental mismatch in communication, I think.
What you have given is essentially problems all on a single “layer”, but what I’m trying to get at is that (a) just because the API/HW interface is shitty doesn’t mean that you have to be overly bound thereby, hence the “thickening”, and (b) your main program should be first and foremost dealing with data-types that are describing your problem.

Now, as to low-level interfacing, that’s fine… but one point I’m getting at is that your bindings to “C ioctl” and “C mmap” have no business being mixed in with SDL: the same “thickening”/abstraction/interfacing that you do for SDL should be done with disk/DB/files so that your main program “doesn’t care” — you’re making things more difficult for yourself: besides translating the datatypes to the mode where they describe your problem-space also catch errors quicker, and prevent the various components from interfering with each other.

Let’s say that your main program is about temperature control for water, so an appropriate datatype for this is Type Temp is range 32..212;, because ice and steam are out of the reasonable bounds for this application.

Now, let’s assume that you have a sensor, and the procedure is such that you have to turn on reading, then read, then turn off; but all of this only after initializing it.

Low-level:

Package Direct_Sensor is
  Type Sensor is private;
  Function Get return Sensor
    with Import, Convention =>, Link_Name => "gts0";
  Procedure Init( Object : in out Sensor )
    with Import, Convention =>, Link_Name => "its1";
  Function Read ( Object : in     Sensor ) return Interfaces.C.Int
    with Import, Convention =>, Link_Name => "rd1";
  Procedure On( Object : in out Sensor )
    with Import, Convention =>, Link_Name => "ons1";
  Procedure Off Object : in out Sensor )
    with Import, Convention =>, Link_Name => "ofs1";
Private
  Type Sensor is record
    Null; -- Whatever stuff the C struct defines.
  End record;
End Direct_Sensor;

Mid-level:

with Main_Program.Types;
private with Direct_Sensor;
Package Managed_Sensor is
  Type Sensor(<>) is limited private;
  Function Create return Sensor;
  Function Read(Object : in    Sensor) return Main_Program.Types.Temp;
Private
  Type Sensor( HW : not null access Direct_Sensor.Sensor ) is limited record
     null; -- Whatever stuff you need.
  end record;
End Managed_Sensor;
-- …
Package Body Managed_Sensor is
  Function "+"(Right : Interfaces.C.Int) return Main_Program.Types.Temp is
  ( Main_Program.Types.Temp(Right) ); -- Whatever conversions you need.

  Function Create return Sensor is
  ( Sensor'( HW => new Direct_Sensor.Sensor(Direct_Sensor.Get) ) );

  Function Read(Object : in    Sensor) return Main_Program.Types.Temp is
    HW : Direct_Sensor.Sensor renames Object.HW.All;
  begin
    -- Note forced to comply with ON, READ, OFF usage protocol.
    Direct_Sensor.On( HW );
    Return Result : constant Temp:= +Direct_Sensor.Read( HW ) do
      Direct_Sensor.Off( HW );
    End return;
  end Read;
End Managed_Sensor;

Note how Managed_Sensor presents to its clients a sanitized form, so that none of the original C import is leaked AND so that the interface-protocol is enforced.

  with Ada.Text_IO; use Ada.Text_IO;                                                                                                                                                                  
                                                                                                                                                                                                  
  procedure Example is                                                                                                                                                                                
     type u8 is mod 2 ** 8;                                                                                                                                                                           
     type Pixel is record                                                                                                                                                                         
       R : u8;                                                                                                                                                                                   
       G : u8;                                                                                                                                                                                   
       B : u8;                                                                                                                                                                                   
     end record;                                                                                                                                                                                  
     type Surface_Type is array (Natural range <>) of Pixel;                                                                                                                        
     type Surface_Access is access all Surface_Type;   
      
     procedure Surface_Updater (Surface : Surface_Access)                                                                                                                                                                   
     is                                                                                                                                                                                                   
     begin                                                                                                                                                                                                                  
       for P in Surface'Range loop
          -- Set pixel
          null;
       end loop;                                                                                                                                                                                                               
     end;                                                                                                                                                                                                                   
     Constrained_Surface   : aliased Surface_Type (1 .. 1024 * 768);                                                                                                                                               
     Unconstrained_Surface : Surface_Access := new Surface_Type (1 .. 1024 * 768);
  begin   
     Surface_Updater (Unconstrained_Surface); -- Works
     Surface_Updater (Constrained_Surface'Access); -- Does not work
  end Example;  

I found a solution to my question. The access type must be unconstrained as Nordic_Dogledding pointed out and this requires using a call to ‘new’ to create the unconstrained access instance.

Thanks for all the help and suggestions!

You’re welcome.
Sorry about “getting into the weeds” a bit, from your initial (as per topic) question.

Just to be precise - this is sloppy wording.
Everything here is a subtype, not a type. Types are anonymous in Ada, what you name in a declaration is the first subtype
There is not much wrong in sloppy wording. Even the RM makes this mistake. You just have to be aware of the difference.