【问题标题】:Passing an Array allocated from a C-routine to Ada将从 C 例程分配的数组传递给 Ada
【发布时间】:2014-04-29 12:15:10
【问题描述】:

将结构/记录数组从 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-Function allocMyrec 会是什么样子,或者正确的策略是什么?

顺便说一句。对于一个元素,可以将 C 指针映射到 access MYREC 。这就是 Gnat-Binding 生成器的作用。但这没有帮助。

高度赞赏的提示。

【问题讨论】:

  • 我不清楚你需要什么。您想要实际调用 C allocMyRec 函数的 Ada 代码,还是想要只执行类似操作而不调用 C 函数的 Ada 代码?
  • 我必须使用 C 库中的分配。所以我想要实际调用 C allocMyRec 的 Ada 代码,但我想在 MYREC_Array 上与 ADA 合作

标签: c arrays pointers ada


【解决方案1】:

我终于用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 包解释解决方案。正如我所见,数组是在编译时创建的,我需要在声明时知道大小。我想在原始 C-Array 上工作,就好像它是一个 MYREC_Array。
  • @user3485675 你可以在执行过程中做同样的事情,没有限制。但是如果你想使用 MYREC_Array,你就必须看看 ajb 解决方案。
  • 如果我知道 C 例程正在创建的记录数,那么 ajb 的解决方案就可以正常工作。但是,如果 C 例程在运行时知道这一点并通过额外的输出参数告诉我分配的大小怎么办?处理此问题的一种方法是创建一个具有希望最大值的数组。预期容量并使用返回的长度。我想知道 String 构造函数是如何告诉 Ada 它有多长的。
  • @user3485675 因为 Ada 不允许指针(访问)到不受约束的数组,所以很难获得指向已分配数组的指针(您可以轻松获得数组的副本)。您可以创建 2 个 C 函数:一个分配并返回大小,另一个返回分配的数组。然后您只需将 ajb 解决方案中的 Size 参数替换为:Size : Int := AllocateMyrec
  • 感谢您的提示。如果我猜对了,您建议子类型声明中的 Size 函数应该分配内存并返回大小,声明 a:MYREC_Array_Subtype := GetMyrecArray 应该返回指向数组的指针,然后复制到 Ada-Array 中(此处为 a ) 因为:=正在复制,对吗?到目前为止,我还没有成功地做到这一点。而且这种策略只有在 Array 有一个实例时才有效。
【解决方案2】:

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

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

我认为 GNAT 编译器会期望 this 指向包含边界的数据紧接着数据。而且您不会让 C allocMyrec 为边界留出空间,除非您可以访问 C 代码并且可以编写一个确实为边界留出空间的包装器。但要做到这一点,您必须确切地知道 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 访问对象;如果不是,那么霍尔特的答案是一个很好的答案。抱歉,如果我误解了您要查找的内容。

【讨论】:

  • 非常感谢这个解决我的问题的解决方案。我还不明白all 到底是干什么用的?如果我将分配数组的大小返回到变量中,策略是什么:MYREC * allocMyRec(int * size).
  • @user3485675 all 关键字代表“一般访问”,即访问所有类型的变量。请参阅en.wikibooks.org/wiki/Ada_Programming/Types/…,但很快,这意味着您可以做的不仅仅是使用简单的access(如访问堆栈上的变量),在 C 中使用指针默认允许这样做。
【解决方案3】:

这是您在预览答案的 cmets 中提出的另一个答案(与之前的答案太长且差异太大,无法仅进行编辑)。

所以你的 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。当我将索引记录作为a(i)'access 传递给C 函数并打印地址(通过“%lx”)时,它似乎与分配的地址不同:26cbe30vs。 26cbe30244d160 但记录访问是正确的。
  • 忘记最后一条地址不同的评论。
  • 如果我们离开 ADA 的美德之路,也可以声明具有最大预期大小的 MYREC_Array_Subtype。这个声明可以发生在程序的任何地方。我们只需要一次。访问数组时,必须注意不要访问超出元素size-1,其中大小由C 函数返回。如果声明的 ADA 数组小于实际大小,程序将在运行时以CONSTRAINED_ERROR 中止。如果数组更大,我们将从 C 中获得经典的覆盖器,并且随机结果将 C 的阴暗面带入 ADA。
【解决方案4】:
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 不应用于访问类型时),您可以以系统相关的方式构造胖指针类型。不是火箭科学。

根据我的经验,我遇到了胖指针不够胖的问题。在 GNAT 中,它的第二个指针指向边界,因此它们必须分配到某个地方。因此,这些定制的胖指针只能驻留在某个为边界提供存储的容器中。当在新位置进行调整时,可能会将胖指针的后半部分修补到新的边界位置。

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

【讨论】:

  • 从语言的角度来看,这可能很危险。受约束的 Ada 字符串类型不需要与 C 字符数组的大小相同(因为 Ada 字符类型可以大于 8 位)。如果要叠加,请使用与 C 兼容的 Interfaces.c.char_array 类型叠加,然后使用 Interfaces.C 中的操作来回转换为 Ada 字符串。
  • 是的,没错,与 C 语言可能存在的差异正在扼杀 Ada 编程的性能和乐趣。存储单元可能不是 8 位的。 ISO允许这么多东西疯狂。同时,所有使用中的架构都很难想象不符合这些假设。如果它们不符合这些假设,那么很可能根本没有 Ada 编译器,而不是存在的异国情调并打破了常见假设。以防万一写几个 pragma Assert 有意义,恕我直言。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-26
  • 1970-01-01
  • 2012-10-15
  • 1970-01-01
  • 2017-03-09
  • 2023-02-06
相关资源
最近更新 更多