从C程序中分配的数组传递到Ada语言

4

将结构体/记录数组从Ada传递到C例程是一件事。在这种情况下,内存管理是由Ada完成的。但是与第三方库进行接口时,通常存在内存管理在C部分完成的问题。

例如:对于C结构体:

typedef struct _MYREC
{
      int n;
      char *str;
 } MYREC;

以下是分配内存的C例程,它返回一个指向具有n个元素的MYREC数组的指针:

MYREC * allocMyrec(int n);

问题在于返回的指针不包含大小信息,这在Ada中进行内存安全计算时是必需的。
在Ada中,我希望使用相应的数组定义来代替(MYREC*)指针:
type MYREC_Array is array (Int range <>) of aliased MYREC;
pragma Convention (C, MYREC_Array);

如何编写一个相应的(大小感知的)Ada函数allocMyrec,或者什么是正确的策略?

顺便提一下,对于一个元素,可以将C指针映射到access MYREC。这就是Gnat绑定生成器所做的。但这并没有什么帮助。

非常感谢任何提示。


我不太清楚您需要什么。您是想要调用C语言的allocMyRec函数的Ada代码,还是只是想要类似于该函数但不调用C函数的Ada代码? - ajb
我必须使用C库中的分配函数。所以我想要调用C allocMyRec 的Ada代码,但我想在MYREC_Array上使用ADA进行操作。 - Florian B.
4个回答

3

我最终使用了Interface.C.Pointers包使其正常工作,这很容易,以下是代码:

with Interfaces.C, Interfaces.C.Pointers ;
use Interfaces.C ;
with Ada.Text_IO ; -- To Check 

procedure MYRECTest is 

    package IIO is new Ada.Text_IO.Integer_IO (Int) ;

    type PChar is access all Char ;

    type MYREC is record
        N : Int ;
        Str : PChar ;
    end record ;
    pragma Convention(C, MYREC); 

    DefaultMyrec : MYREC := (0, null) ; 

    type MYREC_Array is array (Int range <>) of aliased MYREC ;
    pragma Convention(C, MYREC_Array); -- Not sure if useful...

    -- Here is the trick
    package PMYREC is new Interfaces.C.Pointers (Int, MYREC, MYREC_Array, DefaultMyrec) ;

    function AllocMyrec (N : in Int) return PMYREC.Pointer ;
    pragma Import (C, AllocMyrec, "allocMyrec");

    P : PMYREC.Pointer := AllocMyrec(5) ;
    StartP : PMYREC.Pointer := P ; -- Initial pointer
    A : MYREC_Array(0..4) := PMYREC.Value(P, 5) ; -- Here is a copy

begin

    for I in A'Range loop
        -- Real access:
        IIO.Put(P.all.N) ;
        P.all.N := P.all.N + 3 ; -- Here you're really accessing the allocated memory, not just a copy
        PMYREC.Increment(P) ; 
        -- 'Fake' access:
        IIO.Put(A(I).N) ;
        A(I).N := A(I).N + 3 ; -- Here you're accessing a copy, so the modification is not made on the allocated memory
    end loop ;
    Ada.Text_IO.New_Line ;

end MYRECTest ;

我测试了上述代码,将C函数中所有_MYREC元素的n属性设置为42 + i并在Ada主体中打印它,它可以正常工作(我得到了42、43、44、45、46)。测试时C函数如下:

typedef struct _MYREC { 
    int n ;
    char *str ;
} MYREC ;

MYREC * allocMyrec (int n) {
    MYREC *res = malloc(n * sizeof(MYREC)) ;
    int i ;
    for (i = 0 ; i < n ; i++) { 
        res[i].n = 42 + i;
    }
    return res ;
}
DefaultMyrec变量在包创建中是无用但必要的。我假设您总是使用length参数与Value函数一起检索值。
有关该包的更多信息: http://www.adaic.org/resources/add_content/standards/05rm/html/RM-B-3-2.html 编辑:原始代码复制了由P指向的内存,因此如果您更新数组A中的任何内容,则不会在分配的内存中更改。实际上,您应该像编辑后的代码中所示直接使用指针P。 编辑:我对MYRECstr属性使用了“愚蠢”的access all Char,但如果需要,您可以(并且应该)使用来自Interfaces.C.Pointers的几乎相同的东西。我测试过了,但不想将其放入答案中,因为它没有添加任何内容。

非常感谢您使用Pointer包解释解决方案。我看到数组是在编译时创建的,我需要在声明时知道大小。我想像处理MYREC_Array一样处理原始的C-Array。 - Florian B.
@user3485675 在执行期间,您可以做同样的事情,没有限制。但是,如果您想使用MYREC_Array,您将不得不查看ajb的解决方案。 - Holt
如果我知道C例程正在创建的记录数量,那么ajb的解决方案就可以正常工作。但是如果C例程在运行时知道这一点,并通过附加的输出参数告诉我分配的大小呢?处理这种情况的一种方法是创建一个数组,希望它具有最大的预期容量并使用返回的长度。我想知道String构造函数是如何告诉Ada它的长度的。 - Florian B.
@user3485675 因为Ada不允许指向未限制数组的指针(访问),所以很难获得指向分配数组的指针(您可以轻松获得数组的副本)。您可以创建两个C函数:一个分配并返回大小,另一个返回已分配的数组。然后,您只需将ajb解决方案中的“Size”参数替换为类似于“Size:Int:= AllocateMyrec”的内容即可。 - Holt
谢谢提示。如果我没听错的话,您建议在子类型声明中的Size函数应该分配内存并返回大小,声明a:MYREC_Array_Subtype := GetMyrecArray应该返回指向数组的指针,然后将其复制到Ada-Array(这里是a),因为:=会进行拷贝,对吗?到目前为止我还没有成功实现这个方法。而且这种策略只在只有一个数组实例的情况下才有效。 - Florian B.
@user3485675 实际上,只要在“分配”和“获取”中保持正确的顺序,它就可以适用于许多实例(但这是一种非常丑陋的方式来实现你想要的...)。我会添加另一个答案,其中包含我所说的代码。 - Holt

0
type MYREC is record
   n: Integer;
   str: System.Address;
end record with Convention => C;

这个记录包含以“未固定”格式存储的数据,也就是说,您不能直接使用它们。相反,每当您需要处理数据时,您必须将其固定:

declare
   Pinned_MYREC : String (1 .. MYREC_Value.n)
     with Import, Address => MYREC_Value.str;
begin
   -- work with Pinned_MYREC
end;

这个任务可以使用闭包(或泛型)自动化。

procedure Query_MYREC
  (MYREC_Value : MYREC;
   Process : not null access procedure (Pinned_MYREC : String))
is
   Pinned_MYREC : String (1 .. MYREC_Value.n)
     with Import, Address => MYREC_Value.str;
begin
   Process.all (Pinned_MYREC);
end Query_MYREC;

而且,通常情况下(当未应用pragma Pack到访问类型时),您可以以系统相关的方式构造fat指针类型。这不是什么高深的科学。

在我的经验中,我曾遇到过fat指针不够fat的问题。在GNAT中,它的第二个指针指向边界,因此它们必须在某个地方分配。因此,这些自定义制作的fat指针只能驻留在提供边界存储的某个容器内。也许在新位置上进行调整时,将fat指针的后半部分修补到新的边界位置。

在这个项目中,我为直接访问Unbounded_String提供了类型安全的字符串视图和编辑器;适用于Linux x86-64的GNAT。在我的项目中,容器是有限的,因此不需要调整。在我的项目中,fat指针是访问判别式,因此它们不会泄漏。


1
从语言角度来看,这可能是危险的。由于 Ada 字符类型可以比 8 位更大,因此受限制的 Ada 字符串类型不需要与 C 字符数组大小相同。如果要进行覆盖,请使用与 C 兼容的 Interfaces.c.char_array 类型,然后使用 Interfaces.C 中的操作进行 Ada 字符串的转换。 - Jere
是的,没错,C语言可能存在的这些差异会影响Ada编程的性能和乐趣。存储单元可能不是8位的。ISO允许很多东西变得随意。同时,所有正在使用的架构都很难想象不符合这些假设。如果它们不符合这些假设,那么Ada编译器就可能根本不存在,除非是存在且违反常见假设的奇特编译器。以防万一,写几个pragma Assert也是我的建议。 - OCTAGRAM

0

抱歉,我认为没有通用的解决方案。如果您声明一个类型,它是对MYREC_Array的访问,例如:

type MYREC_Array is array (Int range <>) of aliased MYREC;
type MYREC_Array_Access is access all MYREC_Array;

我认为GNAT编译器会期望这个指针指向的数据包含紧随其后的边界数据。除非你可以访问C代码并编写一个留出边界空间的包装器,否则你无法让C中的allocMyrec留出边界空间。但是要做到这一点,你必须确切地了解GNAT编译器的工作原理,答案将与特定实现相关联。如果有一些特殊的声明可以告诉GNAT保持边界分离,我不知道。

有些编译器可能能够处理这个问题;例如,Irvine Compiler的Ada编译器将边界信息作为MYREC_Array_Access类型的一部分而不是与数据连续存储。这种方法有优点和缺点。对于像那样的编译器,你可以构造一个指针,它具有你想要的边界,并指向allocMyrec返回的数据。但这样做需要使用未经检查的操作,并且高度依赖于实现。

在某些情况下,你可以做类似于这样的事情:

procedure Do_Some_Work (Size : Integer) is
    subtype MYREC_Array_Subtype is MYREC_Array (1 .. Size);
    type MYREC_Array_Access is access all MYREC_Array_Subtype;
    function allocMyrec (Size : Interfaces.C.int) return MYREC_Array_Subtype;
    pragma Import (C, allocMyrec);
begin
    ...

现在,由于边界已经内置到子类型中,它们不需要在任何地方存储在内存中;因此这将起作用,并且每当您引用allocMyrec返回的数组的元素时,编译器将确保索引在范围1..Size内。但是您将无法在Do_Some_Work之外使用此结果。您将无法将访问对象转换为在Do_Some_Work之外定义的任何其他访问类型。因此,这是一个相当有限的解决方案。

编辑:我假设您想要指向数组的Ada访问对象;如果不是,则Holt的答案是一个很好的答案。如果我误解了您要寻找什么,那么很抱歉。


非常感谢您提供的解决方案,它将对我的问题起作用。但是我还没有完全理解all的作用是什么?如果我用一个变量来获取已分配数组的大小:MYREC * allocMyRec(int * size),那会有什么策略呢? - Florian B.
@user3485675,“all”关键字代表“通用访问”,即可以访问所有类型的变量。请参见http://en.wikibooks.org/wiki/Ada_Programming/Types/access#General_access,简而言之,这意味着您可以做更多事情,而不仅仅是使用C语言中默认允许的指针(如访问堆栈上的变量)来进行简单的“访问”。 - Holt

0

这是对你在预览答案评论中提出的另一个回答(太长了,与之前的答案差异太大,只进行编辑不合适)。

所以你的C代码看起来像:

typedef struct { ... } MYREC ;

MYREC *last_allocated ;

// Allocate an array of N MYREC and return N.
// The allocated array is "stored" in last_allocated.
int alloc_myrec (void) { ... }

MYREC* get_last_allocated (void) {
    return last_allocated ;
}

然后是你的Ada主体:

procedure MYREC_Test is

    type MYREC is record
        ...
    end record ;
    pragma Convention(C, MYREC) ;

    -- Global and unconstrained array
    type MYREC_Array is array (Int range <>) of aliased MYREC ;
    pragma Convention(C, MYREC_Array);

begin

    declare

        -- Allocate and retrieve the array
        Size : Int := AllocMyrec ;
        subtype MYREC_Array_Subtype is MYREC_Array (1 .. Size);
        type MYREC_Array_Access is access all MYREC_Array_Subtype;
        function GetAlloc return MYREC_Array_Access;
        pragma Import (C, GetAlloc, "get_last_alloc");

        MyArray : MYREC_Array_Access := GetAlloc ;

    begin

    -- Do whatever you want with MyArray

    end ;

end ;

正如我在之前的评论中所说,这种方式有点丑陋。 Interfaces.C.Pointers 包旨在实现您想要的功能,并且如果将所需的一切都放入包中,使用起来会更容易。


非常感谢您的解释!确实我已经成功让它工作了。为了符合从0开始的C数组范围,我必须写成 0 .. Size -1。当我把一个索引记录传递给C函数时,用 a(i)'access 访问并打印地址(通过“%lx”),似乎与分配的不同: 26cbe3026cbe30244d160,但是记录访问是正确的。 - Florian B.
忘记最后一个带有不同地址的评论。 - Florian B.
如果我们离开ADA的道德路径,也可以声明具有最大预期大小的MYREC_Array_Subtype。这个声明可以在程序的任何地方发生。我们只需要它一次。访问数组时必须注意不要超出元素“size-1”的访问范围,其中size由C函数返回。如果ADA数组声明比实际大小小,程序将在运行时中止并显示“CONSTRAINED_ERROR”。如果数组较大,则会得到来自C的经典覆写器和随机结果,从而将C的黑暗面带入ADA。 - Florian B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接