ghdl / ghdl-cosim

Documentation with code examples about interfacing VHDL with foreign languages and tools through GHDL
https://ghdl.github.io/ghdl-cosim
Apache License 2.0
46 stars 9 forks source link

How to call C functions with opaque pointers (VHPIDIRECT) #30

Open augustofg opened 2 years ago

augustofg commented 2 years ago

Hi,

I'm developing a library to allow my VHDL testbenches to be able to listen to TCP connections and send / receive data. The library is written in a object oriented sytle, in which each function receives a pointer to a struct that stores the object internal state, for example:

struct TcpObject {
  int a, b;
};
typedef struct TcpObject TcpObject;

TcpObject* new_tcp_listener();
int tcp_read_data(TcpObject* obj, char* data);
void delete_tcp_listener(TcpObject* obj);

Now, I couldn't find in the documentation and examples what VHDL type to use for storing an opaque pointer that comes from the C code. The best I could think was using an 'access type', but you can't call functions with an 'access type' argument:

entity tb is
  generic (
    g_TCP_PORT : natural := 14000
  );
end tb;

architecture arch of tb is
  type t_tcp_listener is access integer;

  -- Works
  impure function new_tcp_listener (tcp_port : natural) return t_tcp_listener is
  begin report "VHPIDIRECT new_tcp_listener" severity failure; end;
  attribute foreign of new_tcp_listener : function is "VHPIDIRECT new_tcp_listener";

  -- This will fail with: type of constant interface "listener" cannot be access type "t_tcp_listener"
  impure function delete_tcp_listener (listener : t_tcp_listener) return natural is
  begin report "VHPIDIRECT delete_tcp_listener" severity failure; end;
  attribute foreign of delete_tcp_listener : function is "VHPIDIRECT delete_tcp_listener";

begin

  process
    variable net: t_tcp_listener;
  begin
    net := new_tcp_listener(g_TCP_PORT);
    wait;
  end process;
end;

Is there some way of invoking C functions with opaque pointers?

Thanks, Augusto.

augustofg commented 2 years ago

Well, as a workaround you can use a procedure instead of a function if you don't need the return value:

entity tb is
  generic (
    g_TCP_PORT : natural := 14000
  );
end tb;

architecture arch of tb is
  type t_tcp_listener is access integer;

  impure function new_tcp_listener (tcp_port : natural) return t_tcp_listener is
  begin report "VHPIDIRECT new_tcp_listener" severity failure; end;
  attribute foreign of new_tcp_listener : function is "VHPIDIRECT new_tcp_listener";

  procedure delete_tcp_listener (variable listener : t_tcp_listener) is
  begin report "VHPIDIRECT delete_tcp_listener" severity failure; end;
  attribute foreign of delete_tcp_listener : procedure is "VHPIDIRECT delete_tcp_listener";

begin

  process
    variable net: t_tcp_listener;
  begin
    net := new_tcp_listener(g_TCP_PORT);
    delete_tcp_listener(net);
    wait;
  end process;
end;

This works, if you don't touch the variable net, so you don't corrupt the underlying object, but it seems not ideal.

tgingold commented 2 years ago

Yes, you are hitting the limitations of vhdl: a function parameter cannot be an access type (or have an access type). You can indeed use a procedure instead. Or simply use handles instead of pointers.

augustofg commented 2 years ago

After some experimentation I've found few caveats:

Nevertheless, I succeed in writing to a constrained std_logic_vector variable via a procedure:

library ieee;
use ieee.std_logic_1164.all;

entity obj_tb is
end obj_tb;

architecture arch of obj_tb is
  type t_my_obj is access integer;

  impure function new_obj (arg : natural) return t_my_obj is
  begin report "VHPIDIRECT new_obj" severity failure; end;
  attribute foreign of new_obj : function is "VHPIDIRECT new_obj";

  procedure read_data_obj(variable obj  : in t_my_obj;
                          variable data : out std_logic_vector(0 to 31))  is
  begin report "VHPIDIRECT read_data_obj" severity failure; end;
  attribute foreign of read_data_obj: procedure is "VHPIDIRECT read_data_obj";

begin
  process
    variable obj: t_my_obj;
    variable data: std_logic_vector(31 downto 0);
  begin
    obj := new_obj(512);
    read_data_obj(obj, data);
    report "Data read: " & to_string(data) severity note;
    wait;
  end process;
end;
#include <stddef.h>
#include <stdlib.h>

typedef struct {
    int data;
} obj;

obj* new_obj(int arg) {
    obj* myobj = malloc(sizeof(obj));
    myobj->data = arg;
    return myobj;
}

typedef struct {
    char data[32];
} read_data_obj_record;

void read_data_obj(obj* myobj, read_data_obj_record* rec) {
    int mydata = myobj->data;
    for (size_t i = 0; i < 32; i++) {
        rec->data[i] = mydata & 0x80000000 ? 0x03 : 0x02;
        mydata = mydata << 1;
    }
}
$ ghdl -a --std=08 test_arguments.vhd
$ ghdl -e -Wl,testobj.c --std=08 obj_tb
./obj_tb 
test_arguments.vhd:26:5:@0ms:(report note): Data read: 00000000000000000000001000000000

Though this seems to contradict the documentation: this record is passed by reference as the first argument to the subprogram. Maybe this is not true when passing access types?

I couldn't get working anything more complex than a single out argument, the record layout seems to be wildly different from what I expected, but I think it is good enough for now.