views:

337

answers:

3

I am having a VB application request a list of users from a C DLL:
VB will ask the DLL how many users there are, and then initialize an array to the appropriate size.
VB will then pass its array by reference to a DLL function, which will fill it with usernames.

I started writing the C function like this: foo(char **bar); which would be treated as an array of strings. But then I realized, I'm going to make each item in the array point to a different C-string (the char *username in the struct userlist linked list) rather than modify the data already being pointed to. The array of arrays is being passed by value: a copy of a list of addresses, so the addresses point to the original data, but modifying the addresses in that copy won't change the list of addresses of the caller (I think, anyways). So, should I be declaring it foo(char ***bar);? This would be a pointer to the array of strings, so that if I change the strings that array is pointing to, it will modify the array of strings the caller (VB) is using....right?

This is my usage so far (haven't tested it yet... I'm still just coding the DLL as of yet, there's no VB front-end to call it thus far)

EXPORT void __stdcall update_userlist(char ***ulist){

  int i = 0;
  userlist *cur_user = userlist_head; //pointer to first item in linked list

  for(; i < usercount_; ++i){
    *ulist[i] = cur_user->username;
    cur_user = cur_user->next;
  }

}
+1  A: 

two asterixes is the way to go.

char*   // is a pointer to a char
char**  // is a pointer to a char pointer
char*** // is a pointer to a pointer to a char pointer - e.g. multi-dimensional array (err...)

I've confused myself :)

Luke Schafer
http://c2.com/cgi/wiki?ThreeStarProgrammerThere's no such thing as char*** (well, there is, but it's pointless): Anything you can do with a char*** you can do with a char**
Tordek
http://c2.com/cgi/wiki?ThreeStarProgrammer
Dave
You should also note that you can do "jagged array" (I think that's the term). So something like {[1, 2, 3], [1, 2], [1, 2, 3, 4, 5]}!
nevets1219
+4  A: 

In general it's not simple to do what you're asking, because VB just doesn't understand C-style ASCIIZ strings and arrays.

If your DLL is not expecting a VB SafeArray of BSTR, you're going to have some difficulty populating it.

It would be simple to have VB pass in an array of Long (C int) by reference to the first element, and you could fill that with the pointers to individual strings. The VB side could copy them to VB strings. But in that case, who disposes of the C strings, and when?

If you create the VB array and fill it with pre-sized strings, you'll still have to deal with a SafeArray on the C side, because you can't pass a single VB string array element by reference and expect to find the remaining strings contiguous to it in memory.

The best, safest method is to have your DLL create a SafeArray of so-called 'Ansi BSTR', and declare the function in VB as returning an array of strings. Then you don't need two calls, because the array bounds will tell the whole story.

===== edit =====

When VB passes a string array to a Declared function it does some voodoo behind the scenes. It first converts all the strings from Unicode to a bastard form commonly known as 'Ansi BSTR'. To C, these look like and can be treated as ASCIIZ or LPSTR except that you can't create or lengthen them in the normal C way, you can only fill them in. On the C side, the passed array looks like ppSA (SAFEARRAY**). The Ansi BSTR are a series of pointers referenced by the pData member of the SafeArray.

You absolutely cannot pass a single string from the array (as char*) and expect to find the rest of the strings contiguous to it in memory. You have to pass the array itself and manipulate it using the SafeArray API (or knowledge of the SA structure).

That's why the best option overall is to do all of this directly in the DLL. Create the array using SafeArrayCreate, then create Ansi BSTRs using SysAllocStringByteLen and place those strings (which are BSTR, so a 4-byte pointer) into the array slots. On return, VB does its voodoo and converts the strings to Unicode for you.

In VB your function would be Declared as returning a String().

Jim Mack
well if I pass the VB SAFEARRAY by reference to a C function expecting a pointer, I can manipulate it that way, right? And if I make sure the VB array contains the proper number of elements by asking the DLL for that number beforehand, then I can just iterate through the array and change the items, can't I? If what you mentioned about a VB array of Longs works simply, then an array of Strings works too, since BSTR is really just char*. I think using the regular dangerous array, and having two calls is better suited to the simplicity of this program, rather than making a SAFEARRAY...
Carson Myers
...My main problem is that, I can modify the strings in the VB array if I pass it to a char**, but wouldn't that make the actual array get passed by value, and only the strings by reference? This would make it impossible to redirect the array to point to different strings, since I would only be changing the copy of the array, and not the data that the original and copy both point to.
Carson Myers
no, you're right, it can't pass strings, only integral types... thanks
Carson Myers
If you mean that the C function is expecting a pointer to char, then no, you can't just locate the strings as pointer elements and fill them in. The DLL must expect a pointer to a SafeArray, and you must dig into that structure to find the pvData element. _That_ pointer can be used to access the strings by individual pointer (it's essentially char[]). And those strings must each be presized large enough to handle whatever you're going to fill them with.
Jim Mack
+1. But just in case anyone reading this is concerned about internationalisation: the VB6 "ANSI BSTR" strings are in the current system code page, and can't always be treated as ASCII. On some systems (e.g. Japanese, Chinese) they will have multiple bytes per character.
MarkJ
An alternative would be to pass a structure from the VB6 containing fixed-length strings. It saves the DLL creating a SafeArray and BSTRs. VB6 does an equivalent Unicode-ANSI conversion, so on a single-byte code page you can treat the fixed length strings as char[] blocks. Obviously you can't resize them - you could null terminate them, as long as you write the VB6 to expect that.
MarkJ
I'd also like to share this link. Microsoft's documentation from VB5 on calling C DLLs from VB6 (reproduced with permission). I'm sure it all still applies for VB6. http://vb.mvps.org/tips/vb5dll.asp
MarkJ
Be aware of one quirk when passing a structure containing FL strings, or any VB strings (it can actually be a useful quirk). When you pass one such UDT, VB does the Ansi<->Unicode thing. But when you pass an array of them, it does not. The strings arrive in the array still containing Unicode.
Jim Mack
A: 

So let me get this straight. Your function fills in an array of strings from data contained in a linked list ?

If you know the size of the list beforehand, you can just pass a char **, but if you do not know the size and need to be able to grow the list, you will need a char ***.

From looking at your code, you seem to already know the length, so you just need to allocate an array of the correct length before you call the function. Here is an example:

void update_userlist(char **ulist)
{
    int i = 0;
    userlist *cur_user = userlist_head;

    for(; i < usercount_; ++i)
    {
        ulist[i] = cur_user->username; // I am assuming that username is a char *
        cur_user = cur_user->next;
    }
}

// This sets up the array and calls the function.
char **mylist  = malloc(sizeof(char*) * usercount_);
update_userlist(mylist);

Update: Here is the difference between the various levels of pointers:

  1. void func1(char *data)
    This passes a copy of a pointer to a C string. If you change the pointer to point to a different string, the calling function will still point to the original string.

  2. void func2(char **data)
    This passes a copy of a pointer to an array of pointers to C strings. You can replace the pointer to any string in the array and the calling function's array will be changed because it has not made a copy of the array, it only points to the caller's array.

  3. void func3(char ***data)
    This passes a pointer to a pointer to an array of pointers to C strings. With this, you can completely replace the entire array. You would only need this level of indirection if you need to grow the array since C arrays cannot be re-sized.

Adam Pierce
the function which needs the array populated is in VB6, so I can't really manipulate any pointers before hand. VB gets the number of strings and then creates an array of strings (I think that's equivalent to char** in C, stuff gets converted away from COM when passing to DLL).
Carson Myers
also, when I change ulist[i], I'm only changing the temporary array-of-pointers that got passed, aren't I? The original array that I passed won't get changed, only the data pointed to by *ulist[i] will be changed...right?
Carson Myers
A char ** is a pointer to the array. So if you change the pointer to any individual string in that array, it changes the original. There is no copy made of the array.
Adam Pierce
if you accept p in a function, you can change p but it won't be changed outside the function. If you accept it as *p, you can change it via *p, but if you supply *s to the function expecting *p, you can change the pointed to value, but if you change p, s won't change. If you want to supply *s to the function, and have the pointer re-directed, you create a pointer to that pointer, and accept **p--right?
Carson Myers
I've updated my answer with some explanation of the different levels of indirection and what they can do. I stand by my original explanation that you only need two asterisks for your application.
Adam Pierce