Basic Question: Interfacing With C Arrays, Somewhat Tangental

I’ve been doing a project with vulkan/sdl/ada for a few days so far. I have a few questions but this one will specifically be about the way I import the array from c, and my understanding of what the basic memory layout will look like inside. I am a new programmer so I may be completely in the incorrect weeds. I tried on my own for 2/3 days before posting this question.

Reasoning: The reason why I want to import the array into ada is because I want to do some checks/elaboration upon the contents of the array without just passing the System.Address and ignoring the rest.

What I am trying with the array:

  • No copying.
  • Follow correct convention.
  • Be able to use it in ada without tons of conversion.
  • The function that I am trying to pass it to is just a binding to vulkan that can take in a c array, so I could really just pass the Extension_Names_Address(System.Address) into the vulkan binding, but I just want to be sure that there are no weird pitfalls that I may be missing.
-- Ada, Get extension names from sdl.
Extension_Names_Count_Var : aliased SDL3_SDL_stdinc_h.Uint32;
Extension_Names_Address   : System.Address :=
  SDL3_SDL_vulkan_h.SDL_Vulkan_GetInstanceExtensions
    (Extension_Names_Count_Var'Access);
Extension_Names_Var       :
        chars_ptr_array (size_t (1) .. size_t (Extension_Names_Count_Var))
        with Import, Convention => C, Address => Extension_Names_Address; -- This is the unhappy line. I don't want to copy it, but I don't really know what this is doing, and reading more is confusing me.
-- Ada, The sdl binding for SDL_Vulkan_GetInstanceExtensions
-- Automatically generated by `gcc -c -fdump-ada-spec -C /filenames`
function SDL_Vulkan_GetInstanceExtensions
  (count : access SDL3_SDL_stdinc_h.Uint32)
   return System.Address  -- /usr/include/SDL3/SDL_vulkan.h:200
with
  Import        => True,
  Convention    => C,
  External_Name => "SDL_Vulkan_GetInstanceExtensions";
// C, The specification for SDL_Vulkan_GetInstanceExtensions.
extern SDL_DECLSPEC char const * const * SDLCALL SDL_Vulkan_GetInstanceExtensions(Uint32 *count);
-- The spec of the vulkan function with a simplified example of passing the array.
-- Over-simplified, but with the correct variable types, so this post isn't made an even longer code dump than it already is.

-- Ada, also an automatically generated c binding.
ppEnabledExtensionNames : System.Address;

// what this variable was in c.
const char* const* ppEnabledExtensionNames;

My secondary method: As seen above the const char* const* is the same in both the return type of the sdl function and intake for the simplified vulkan function, therefore, I could just take the System.Address of the first return and send it to the vulkan function. I have tried this and it seems to work correctly.

Third method: I suppose I could import the array into ada to do those extra checks and send the Extension_Names_Address just to go faster too. So using both might have benefits.

Expected basic memory layout for each of the three methods:
1: C array, Extension_Names_Address points to that array, Extension_Names_Var copies that array and now holds a correctly sized array in ada, vulkan would be taking in Extension_Names_Var and not the original c array with my code above.
2. C array, Extension_Names_Address points to that array, vulkan would take in Extension_Names_Address which points to the original c array.
3. Same as the above two methods, but vulkan would take method 2’s way of in-taking the array, and ada would hold its own copy of the array for me to do further work with. So vulkan would be pointing to the System.Address of the original c array, and I would have a copy in ada.

Summary of questions:
Is my current way with importing the array from the address okay?
Is my secondary method of sending the System.Address okay?
Is my third method of using both methods okay?
Is the expected memory layout I have good understanding?
How can I import the array using my existing first method without copying. Or maybe I have it correct already?

If you have any other opinions please send, I know this is a long post, but I think that I’m somewhat on the right track. I’ve been suffering for days, please understand that this is indeed a stupid first programming project, but I am stubborn. I dropped rust because I didn’t like how many times it used asterisks and other characters, and I’ve been happy with ada syntax so that’s a success so far. I have never really written ada or c before.

Basic answer. C does not have arrays. It has pointers.

You cannot use arrays with no arrays, so you must decide whether you convert pointers to memory to arrays or else keep on using pointers.

Ada bindings usually support both ways. Thin bindings keep pointers, in you case it is char**. Thick bindings would convert char* to Ada String and pack them into a container or query a name by index 1..Count.

Your example:

function SDL_Vulkan_GetInstanceExtensions (Count : not null access Unsigned_32)
   return access chars_ptr with ...; -- Thin bindings

You can also instantiate Interfaces.C.Pointers with chars_ptr and use the Pointer type from there as the result SDL_Vulkan_GetInstanceExtensions. Then you would enjoy C pointer arithmetic in its full glory.

3 Likes

Here is a simple example that uses C arrays in Ada:

[ scores.c ]

#include <stdint.h>

uint8_t  grades[] = { 60, 90, 80, 100, 50};
int      length   = sizeof(grades) / sizeof(uint8_t);

[ average.adb ]

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
with System; use System;

procedure Average is
  type u8 is mod 2**8;
  type Grades_Array is array (Natural range <>) of u8;
  Length  : Integer with Import => True, Convention => C;
  Grades  : Grades_Array (1 .. Length) with Import => True, Convention => C;
  Sum     : Natural := 0;
  Average : Float;
begin
  for Index in Grades’Range loop
    Put_Line ("Including: " & u8’Image (Grades (Index)));
    Sum := Sum + Natural (Grades(Index));
  end loop;
  Average := Float (Sum) / Float (Grades’Length);
  
  Put ("Average:   ");
  Put (Average, Fore => 3, Aft => 2, Exp => 0);
  New_Line;
end Average;

[ average.gpr ]

project Average is
   for Languages use ("Ada", "C");
   for Source_Dirs use (".");
   for Object_Dir use "obj";
   for Create_Missing_Dirs use "True";
   for Exec_Dir use ".";
   for Main use ("average.adb");
end Average;

Build it.. (using gprbuild)

gprbuild
using project file average.gpr
Compile
[Ada]          average.adb
[C]            scores.c
Bind
[gprbind]      average.bexch
[Ada]          average.ali
Link
[archive]      libaverage.a
[index]        libaverage.a
[link]         average.adb

And voila..

./average
Including:  60
Including:  90
Including:  80
Including:  100
Including:  50
Average:    76.00
4 Likes

Thank you both for your responses.

I’m going to go review the basics, missing that point about c not having arrays is a major facepalm on my part. :sweat_smile: Newbie programming does that I guess haha. All that typing and deliberation, and I miss that. Damn.

I’m off to go read a few books, I’m obviously in sore need of revising. I’ll return with progress! (hope)