前言:
四年前在delphi ktop有人提到看盤軟體配合DDE技術,在用delphi實作時發生了亂碼加在後面的情形。http://delphi.ktop.com.tw/board.php?cid=30&fid=71&tid=97629
當時沒時間,只是大致上猜其原因。四年後的今天,又有一位朋友說他找到如何解的方式。於是讓我再度燃起追其真正的原因。
原因:
TDDEClientItem的Text屬性在某些情形下會得到亂碼加在資料的後面。原因追到VCL源碼去後發現,Text的值與下面的函式有關:
procedure TDdeCliItem.StoreData(DdeDat: HDDEData);
var
Len: Longint;
Data: string;
I: Integer;
begin
if DdeDat = 0 then
begin
RefreshData;
Exit;
end;
Data := PChar(AccessData(DdeDat, @Len)); //這裡就是問題的所在!
if Data <> '' then
begin
FCtrl.Lines.Text := Data;
ReleaseData(DdeDat);
if FCliConv.FormatChars = False then
begin
for I := 1 to Length(Data) do
if (Data[I] > #0) and (Data[I] < ' ') then Data[I] := ' ';
FCtrl.Lines.Text := Data;
end;
end;
DataChange;
end;
問題在Data := PChar(AccessData(DdeDat, @Len)); 這行上。其中,AccessData這個函式又叫了Win32API的DdeAccessData(DdeDat, pDataLen); 這個api回傳的是一個指向Byte型別的指標。經強轉為PChar (指向Char)的指標後,再透過內delphi的內建的AnsiString建構式把PChar指的字串變成AnsiString。至此,感覺好像沒錯… 但深入的思考,若傳回的指標的結尾null 值放在不正確的位置上,就會造成附加一些亂碼出現。然而,有可能null值的位置錯了嗎? 因為值的來源是dde server上傳來的,問題追入dde server 相關的api上。經了解,它與win32api裡的DdeCreateDataHandle 這個函式有關。查msdn得:
HDDEDATA WINAPI DdeCreateDataHandle(
_In_ DWORD idInst,
_In_opt_ LPBYTE pSrc,
_In_ DWORD cb,
_In_ DWORD cbOff,
_In_opt_ HSZ hszItem,
_In_ UINT wFmt,
_In_ UINT afCmd
);
其中,第三個參數,是關鍵,它描述出要填入內容的長度為何。仔細再看第三參數的說明:
The amount of memory, in bytes, to copy from the buffer pointed to by pSrc. (include the terminating NULL, if the data is a string).
哇重點來了啦! 有沒有,當server把資料傳入時,若是傳字串,長度要「自己加1」因為要含null這個結尾。
於是假設寫server的那個人,忘了這件事… 我覺得這種事常常會發生的。那麼就會造成null值沒被放入記憶區塊。舉例來說,若要放入 ABC這三個字在memory上。假設該memory上目前的值是一堆亂的東西,比如說長這樣:
31 32 33 34 35 36 37 38 00 31 32,(十六進位),若當它字串來看是 '12345678'
當把ABC放入 (其實是copy上去)那這區塊會變成
甲) 41 42 43 34 35 36 37 38 00 31 32 (當字串來看是ABC45678,若指定3的長度)
或是
乙) 41 42 43 00 35 36 37 38 00 31 32 (當字來看就是ABC,若指定4的長度,因null值有進去啦)
到目前為止,似乎命運操在寫server 程式人的手上,但沒那麼悲觀。再回首看上面講的DdeAccessData(DdeDat, pDataLen);這個api。第二個參數是那道光! 它指示出資料本身是多長,它呼應了DdeCreateDataHandle的第三個參數!! 假始,以剛的例子來說,server的實作是把ABC用長度3來copy進記憶區塊中,那麼這個pDataLen就會是3。有這個值就有救了!
跳離一下上面的論述。因朋友提到他用TDdeClientConv.RequestData的方法取值就正確! 於是我追入源碼看:
function TDdeClientConv.RequestData(const Item: string): PChar;
var
hData: HDDEData;
ddeRslt: LongInt;
hItem: HSZ;
pData: Pointer;
Len: Integer;
begin
Result := nil;
if (FConv = 0) or FWaitStat then Exit;
hItem := DdeCreateStringHandle(ddeMgr.DdeInstId, PChar(Item), CP_WINANSI);
if hItem <> 0 then
begin
hData := DdeClientTransaction(nil, 0, FConv, hItem, FDdeFmt,
XTYP_REQUEST, 10000, @ddeRslt);
DdeFreeStringHandle(ddeMgr.DdeInstId, hItem);
if hData <> 0 then
try
pData := DdeAccessData(hData, @Len); // 從這裡開始看
if pData <> nil then
try
Result := StrAlloc(Len + 1); // 哇,有聰明,會用len + 1
Move(pData^, Result^, len); // data is binary, may contain nulls
Result[len] := #0; // 自己在最後面塞 null 值,讚啦!
finally
DdeUnaccessData(hData);
end;
finally
DdeFreeDataHandle(hData); // 到這裡結束關鍵
end;
end;
end;
是不是? 更加驗證了我開始的推論!! 同樣都是在取dde server送來的資料,竟不同的人員(絕對是不同的borland工程師,寫法就是不同)用不同的寫法。RequestData方法的實作是很嚴僅的!! 它考慮了server 的實作人員可能會忘了把null的長度也算進去啦! 但TDdeCliItem.StoreData的實作人員就不再乎,他認為那是寫server的人員自己該注意的? 事實上,若你DDE server是用delphi寫的,嘿~~ borland的人員是有加null的長度的! 類似如下:
hszCmd := DdeCreateDataHandle(ddeMgr.DdeInstId, Cmd, StrLen(Cmd) + 1,
0, 0, FDdeFmt, 0);
所以,用delphi寫的DDE server來驗本題目,是完全不會錯! 這可能也是實作TDdeCliItem.StoreData函式的人員測式的時候總不會發現問題,於是就這樣不嚴僅下去…
解法:
了解以上的來龍去脈後,解就不難,我修正如下:
procedure TDdeCliItem.StoreData(DdeDat: HDDEData);
var
Len: Longint;
Data: string;
I: Integer;
begin
if DdeDat = 0 then
begin
RefreshData;
Exit;
end;
Data := PChar(AccessData(DdeDat, @Len)); //這裡就是問題的所在!
if Data <> '' then
begin
SetLength(Data, Len); //加入這行應該就可以搞定! 我沒delphi可測。
FCtrl.Lines.Text := Data;
至於改好後,要怎麼來變更delphi的library ? 這又是另一個題目…我認為應該是將ddeman.pas檔照上面改好後,在delphi裡,用install component的功能,選ddeman.pas檔,然後compile,然後會出錯,但沒關係,會得到ddeman.dcu,然後把這個檔去蓋delphi安裝目錄下的那個。請自行試看看。因我沒有delphi,我都是用c++ builder,故無法驗證後再保證沒錯啦! 請見諒!
結論:
不知這樣的結果算不算是bug? 假始寫server的人,用vc++寫,也注意到要含null一起copy到記憶體區塊,那就不會出事。但,最佳的解法是依DdeAccessData(hData, @Len); 中的len來決定一個正確的字串,而不是依null值的所在位置。(因null值可能是在正確字串後的任何一個地方出現的)。我會找時間去Embarcadero report這個bug?,希望在未來的版本得到修正!
留言列表