3

I am using Delpi XE6, if it matters.

I am writing a DLL that will get a response from Google's Directions API, parse the resulting JSON and determine the total distance of the trip.

GetJSONString_OrDie works as intended. It takes a URL and attempts to get a response from Google. If it does, it returns the JSON string that Google returns.

ExtractDistancesFromTrip_OrDie should take in a JSON string, and parse it to get the total distance of each leg of the journey, and stick those distances in an array of doubles.

SumTripDistances takes in an array of doubles, and sums up the array, and returns the total.

The curious thing is, if I take these functions out of the DLL and put them in a project and call them like that, it works as intended. Only when the are stuck into a DLL is when things seem to go wrong. As said, GetJSONString_OrDie works as intened and returns a JSON string - but when stepping through the DLL's ExtractDistancesForTrip, the passed PChar string is ''. Why is that?

unit Unit1;



interface
uses
  IdHTTP,
  IdSSLOpenSSL,
  System.SysUtils,
  System.JSON;

type
  arrayOfDouble = array of double;

function GetJSONString_OrDie(url : Pchar) : Pchar;
function ExtractDistancesForTrip_OrDie(JSONstring: Pchar) : arrayOfDouble;
function SumTripDistances(tripDistancesArray: arrayOfDouble) : double;

implementation



{ Attempts to get JSON back from Google's Directions API }
function GetJSONString_OrDie(url : Pchar) : PChar;
var
  lHTTP: TIdHTTP;
  SSL: TIdSSLIOHandlerSocketOpenSSL;
begin
  {Sets up SSL}
  SSL := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  {Creates an HTTP request}
  lHTTP := TIdHTTP.Create(nil);
  {Sets the HTTP request to use SSL}
  lHTTP.IOHandler := SSL;
  try
    {Attempts to get JSON back from Google's Directions API}
    Result := PWideChar(lHTTP.Get(url));
  finally
    {Frees up the HTTP object}
    lHTTP.Free;
    {Frees up the SSL object}
    SSL.Free;
  end;
end;

{ Extracts the distances from the JSON string }
function ExtractDistancesForTrip_OrDie(JSONstring: Pchar) : arrayOfDouble;
var
  jsonObject: TJSONObject;
  jsonArray: TJSONArray;
  numberOfLegs: integer;
  I: integer;
begin
  //raise Exception.Create('ExtractDistancesForTrip_OrDie:array of double has not yet been '+
  //                        'implemented.');

  jsonObject:= nil;
  jsonArray:= nil;
  try
    { Extract the number of legs in the trip }
    jsonObject:= TJSONObject.ParseJSONValue(TEncoding.ASCII.GetBytes(JSONstring), 0) as TJSONObject;
    jsonObject:= (jsonObject.Pairs[0].JsonValue as TJSONArray).Items[0] as TJSONObject;
    jsonArray:= jsonObject.Pairs[2].JSONValue as TJSONArray;
    numberOfLegs:= jsonArray.Count;

    {Resize the Resuls arrayOfDouble}
    SetLength(Result, numberOfLegs);

    {Loop through the json and set the result of the distance of the leg}
    {Distance is in km}
    for I := 0 to numberOfLegs-1 do
    begin
      Result[I]:= StrToFloat((((jsonArray.Items[I] as TJSONObject).Pairs[0].JsonValue as TJSONObject).Pairs[1]).JsonValue.Value);
    end;


  finally
    jsonObject.Free;
    jsonArray.Free;
  end;

end;


function SumTripDistances(tripDistancesArray: arrayOfDouble) : double;
var
  I: integer;
begin
  //raise Exception.Create('GetDistanceBetweenPoints_OrDie:double has not yet been ' +
 //                        'implemented.');

 Result:= 0;
 {Loop through the tripDistancesArray, and add up all the values.}
 for I := Low(tripDistancesArray) to High(tripDistancesArray) do
    Result := Result + tripDistancesArray[I];


end;

end.

Here is how the functions are being called:

program Project3;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  ShareMem,
  IdHTTP,
  IdSSLOpenSSL,
  System.SysUtils,
  System.JSON,
  System.Classes;

type
  arrayOfDouble = array of double;

  testRecord = record
    testString : string;
    testInt : integer;
  end;

function GetJSONString_OrDie(url : Pchar) : PWideChar; stdcall; external 'NMBSGoogleMaps.dll';

function ExtractDistancesForTrip_OrDie(JSONstring: pchar) : arrayOfDouble;  stdcall; external 'NMBSGoogleMaps.dll';

function SumTripDistances(tripDistancesArray: arrayOfDouble) : double; stdcall; external 'NMBSGoogleMaps.dll';


var
  jsonReturnString: string;
  jsonReturnString2: Pchar;
  doubles: arrayOfDouble;
  totalJourney: double;
  uri:Pchar;

  a : testRecord;
  begin
  try
    uri:= 'https://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=Charlestown,MA|Lexington,MA&key=GETYOUROWNKEY';


    { TODO -oUser -cConsole Main : Insert code here }
    jsonReturnString:= GetJSONString_OrDie(uri); //On step-through, uri is fine.

    jsonReturnString2:= stralloc(length(jsonreturnstring)+1);
    strpcopy(jsonreturnstring2, jsonreturnstring);
    jsonreturnstring2 := 'RANDOMJUNK';

    doubles:= ExtractDistancesForTrip_OrDie(jsonReturnString2); //On step-through, jsonReturnString2 is seen as '', rather than 'RANDOMJUNK'
    totalJourney:= SumTripDistances(doubles);
    WriteLn('The total journey was: ');
    WriteLn(totalJourney);

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  readln;
end.

When the first function is called, GetJSONString_OrDie, uri is passed in and is correct when viewed or printed out. However, after creating a random string and passing it into ExtractDistancesForTrip_OrDie, the function only sees ''. If I were to change ExtractDistancesForTrip_OrDie to accept an integer, or a record, or anything - it sees either garbage or random data. It does not matter if I pass by reference or value.

Acorn
  • 1,147
  • 12
  • 27

1 Answers1

5

Your calling conventions don't match. Your functions use register calling convention but you import them as if they were stdcall. That's why the parameters you pass are not arriving.

Use stdcall both when you implement the function, and also when you import it.

In GetJSONString_OrDie you are returning a pointer to the buffer of a local variable. As soon as the function returns, that local variable is destroyed and the pointer is invalid. You'll need to find a different way to pass string data out of this function.

Returning a string would normally be fine. But since this function is exported from a DLL that won't work. You'd need either a caller allocated buffer, or a buffer allocated on a shared heap.

Finally, dynamic arrays are not valid interop types. You should not pass these across module boundaries.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Are you talking about `GetJSONString_OrDie`? I am able to view and edit the PChar that is returned by it, wouldn't that not be possible if it were getting destroyed after the function returns? – Acorn Jun 17 '15 at 12:50
  • You are referring to memory that has been deallocated. The memory manager might not have overwritten that memory, or returned it to the OS. Such bugs can be confusing because the code can appear to work some times. – David Heffernan Jun 17 '15 at 12:51
  • Makes sense! I'm fairly new to Delphi, could you provide an example of "You'd need either a caller allocated buffer, or a buffer allocated on a shared heap." ? I'm assuming I would allocate a string on the heap with some size, and then copy the PChar into it? But I'm not sure how to go about doing that(or if that is even the correct thing to do). – Acorn Jun 17 '15 at 13:05
  • There are many examples around. Exactly what to use depends on many factors that I don't know. Is this function exported from the DLL? What consumes it? Can the caller allocate the buffer? I don't know any of those details. – David Heffernan Jun 17 '15 at 13:08
  • An example here, [In Delphi in my DLL do I have to allocate the return pchar of a function](http://stackoverflow.com/q/4269622/576719). – LU RD Jun 17 '15 at 13:13
  • @LURD, I had been messing around with something similar to how it was implemented in that answer. `Buffer:= lHTTP.Get(url);` `Result:= StrAlloc(length(Buffer)+1);` `StrPCopy(Result, Buffer);` However, the pointer still seems to be getting destroyed when it shouldn't be? – Acorn Jun 17 '15 at 13:50
  • You are allocating inside the dll proc. The caller must do the allocation before calling the dll proc. – LU RD Jun 17 '15 at 13:53
  • The second half of the linked SO answer says that I do not need to do that, if it is not possible/feasible. "_If this is not an option for you, then you need to have the DLL allocate the memory, return it to the app for use, and then have the DLL export an additional function that the app can call when it is done to pass the pointer back to the DLL for freeing:_" Which is the approach I was using – Acorn Jun 17 '15 at 13:57
  • No, that's not the approach that you were using. And Remy misses one option which is to allocate on a shared heap rather than exporting a deallocator. You didn't answer any of my questions. – David Heffernan Jun 17 '15 at 14:00
  • Care to explain how it isn't? Granted, I forgot to update my code - but in my edit section I have clearly outlined what I was doing, and it is completely identical to what Remy had provided. As for your questions, those seemed superfluous to me. All I wanted to know was if my idea to an approach was correct, or if you could provide a crude example. If you do want answers, then yes, another function call, no. In the future, only one function that acts as a wrapper for all three will be exported. – Acorn Jun 17 '15 at 14:22
  • Remy suggests that you allocate memory, and return that to the caller. You are allocating memory, deallocating that memory, and returning the pointer to the now deallocated memory. – David Heffernan Jun 17 '15 at 14:34
  • When & how is it getting deallocated? I've commented out the only `free` calls. Additionally, if I do something silly in the main program, like this: ` jsonreturnstring2 := 'SGFSLGSKKKKKK';` and pass it into `ExtractDistancesForTrip_OrDie`, when I step thru `ExtractDistancesForTrip_OrDie`, the parameter passed in is still viewed as `''`. No deallocation is happening anywhere, no `free`'s are being called. Even if I make some silly random string and pass it through, it is still being read as `''`, which makes no sense. – Acorn Jun 17 '15 at 14:38
  • The objects behind string variables are automatically managed. When there are no more references to them, they are destroyed. In your code, `lHTTP.Get(url)` is assigned to an implicitly created local variable of type string. When the function returns, the local variables leave scope and that implicit local is destroyed. – David Heffernan Jun 17 '15 at 14:44
  • As for my questions being superfluous, I don't really think so. I still don't really know what will consume this exported function. I mean by that what sort of programming language. Is the consumer always another Delphi module? In which case you might opt for sharemem. Otherwise a COM BSTR might be a reasonable choice. Before taking any of these decisions you will need a clearer understanding of the problem. Not recognising that you are returning an invalid pointer is your real hurdle. You have to get on top of that before you can expect to make informed decisions. – David Heffernan Jun 17 '15 at 14:49
  • That's nice and all, but you've ignored my last comment. Nothing is being passed into `ExtractDistanceForTrip_OrDie`. If I pass an integer, it's a random integer. If I pass a pchar, it's empty. It can declare it and write it out to the console before passing it into the function, and print it out after the function, and it's fine. For whatever reason, when it is used as a parameter the data is lost. That is the actual problem. Not because lHTTP object goes out of scope and is destructed (which doesn't make sense to me, it was my understanding that Delphi doesn't have a memory manager but w/e) – Acorn Jun 17 '15 at 15:58
  • If nothing is arriving as input then I suggest you show us how you call the function. If you don't understand why the return value handling of your code is broken then you will need to improve your knowledge to make headway. – David Heffernan Jun 17 '15 at 16:17
  • Anyway, perhaps you are importing as `stdcall`. – David Heffernan Jun 17 '15 at 16:26
  • Updated with how the functions are called. Your last comment and your edit are confusing - your comment suggests to not import as stdcall, but your edit suggests that is the way to import, and not with register. – Acorn Jun 17 '15 at 17:32
  • 1
    My guess was correct. Make them both stdcall. The dyn array return value is no good either. – David Heffernan Jun 17 '15 at 17:49