Board logo

Subject: CnWizards 在 Windows 2003 下多语乱码的修补手记 [Print This Page]

Author: Passion    Time: 2006-10-11 23:22     Subject: CnWizards 在 Windows 2003 下多语乱码的修补手记

很早就有朋友报告过Windows 2003下D7的专家包窗体出现乱码的毛病,我却一直没找到原因,也考虑过窗体字体的字符集问题,但当时想的是,既然其他系统下都是一样地处理窗体的字符集,那么单就2003下乱码,真没这个道理,于是就一直陷在这个圈子里没去想法解决。直到昨天一位朋友提到专家包的语言切换菜单不见了,这才意识到问题的严重性。没有语言菜单说明多语包中没有语言条目,而在语言文件都正常存在的情况下,那最大的可能就是多语包在2003下初始化失败了!正巧昨晚测试2003多语问题的西门诸葛寄来了他的XML跟踪记录,一看,果然是多语包在从文件读入多语条目时产生了异常,导致无一语言正确加载。这种情况下对于窗体便根本没进行翻译,而窗体本身为了照顾到不同系统的多语需要,初始字体的字符集是Default,并未针对中文改成GB2312,这便是2003下窗体乱码的根本原因。
剩下的问题就是研究为啥多语包会初始化失败。有朋友报告把Windows2003的DEP关掉就OK了,而我也想不出DEP和多语的初始化有啥关系,只有硬着头皮跟下去。跟踪记录显示出问题的是访问Sysutils.Languages时出的问题。跟进多语包内部,确定是在判断某语言号是否合法的时候调用了Sysutils.Languages函数,大概是第一次调用,所以会Create。一个SysUtils单元里头的东西在Create的时候会有问题?抱着不愿相信的态度看了看TLanguages.Create的源码:

constructor TLanguages.Create;
type
  TCallbackThunk = packed record
    POPEDX: Byte;
    MOVEAX: Byte;
    SelfPtr: Pointer;
    PUSHEAX: Byte;
    PUSHEDX: Byte;
    JMP: Byte;
    JmpOffset: Integer;
  end;
var
  Callback: TCallbackThunk;
begin
  inherited Create;
  Callback.POPEDX := $5A;
  Callback.MOVEAX := $B8;
  Callback.SelfPtr := Self;
  Callback.PUSHEAX := $50;
  Callback.PUSHEDX := $52;
  Callback.JMP     := $E9;
  Callback.JmpOffset := Integer(@TLanguages.LocalesCallback) - Integer(@Callback.JMP) - 5;
  EnumSystemLocales(TFNLocaleEnumProc(@Callback), LCID_SUPPORTED);
end;

原来如此!EnumSystemLocales需要一个回调函数,而TLanguages自身提供的供回调的是类成员函数,于是用了这么个法子来转换(这法子似乎也在savetime的作品里头看到过),导致被调用的函数的入口在局部变量这个数据区中,而这在开了DEP的情况下,很可能会出异常(之所以只说很可能,是因为我在XP上开了DEP测试时,并未每次都产生异常。编译一个多语包的测试例子出了异常,而专家包自身却没有,不明白咋回事)。然后关掉DEP再跑就没了。
原因找出来了。既然我们没法修改TLanguages自身的行为,也不能强行关闭用户机器上的DEP,那么就只有在初始化失败后做一点补救工作了。原来的窗体为了多语设置方便,虽然其中的文字是中文的,但初始字体却是默认的字符集,只在多语翻译此窗体时才设置为正确的字符集。现在加上的补救措施就那么一点点:多语包无语言条目时,将窗体的字体字符集手工设置为GB2312,就OK了。虽然对于英文用户来讲,中文不认识,而且在英文内码下中文也可能会是乱码,但那已经不是我们能控制的了,除非我们动个大手术把所有窗体的dfm里头的中文全改成英文。
本来还有条路,可以考虑改成多语包的多语条目和系统的Languages不挂钩,但改动太大了,暂不考虑。

幸亏有西门诸葛的帮助测试,才能让这个问题(可能)顺利修补掉,在此表示感谢。
Author: shenloqi    Time: 2006-10-12 09:46

我一般都直接把对Delphi的DEP给关掉的。
会不会出问题跟编译结果和CPU等都相关,跟数据段中的代码所在的内存页的标志相关,而且可能Intel的兼容性好于AMD。
Author: Passion    Time: 2006-10-12 12:13

今早想出一个法子,打算抛弃Languages对象,自行写个新的TCnLanguages,换个法子来获得系统语言列表以绕过 DEP。
Author: VictorWoo    Time: 2006-10-12 13:11

虽然看不是很明白,但是注意到“EnumSystemLocales需要一个回调函数,而TLanguages自身提供的供回调的是类成员函数”。之前我用类成员函数作为回调函数的时候会出现一些怪问题。Passion能否指点一下?
另,如果声明回调的时候写成 procedure of object,是否能解决?
可能不太切题,谢谢指教
Author: oldnew    Time: 2006-10-12 14:21

关注4楼
Author: Passion    Time: 2006-10-12 14:48

类的成员函数在被调的时候是需要一个Self参数的,这样成员函数才能使用Self指针。
而提供给Windows的回调函数在被Windows调的时候基本不会传入一个合法的Self来,而且类成员函数和回调函数的调用方式可能也不同(stdcall或fastcall等),所以必须转换。

一句话,传回调地址时必须保证这个回调函数被正确调用,否则就白传了。
Author: zjy    Time: 2006-10-12 15:38



QUOTE:
Originally posted by VictorWoo at 2006-10-12 13:11:
虽然看不是很明白,但是注意到“EnumSystemLocales需要一个回调函数,而TLanguages自身提供的供回调的是类成员函数”。之前我用类成员函数作为回调函数的时候会出现一些怪问题。Passion能否指点一下?
另,如果声 ...

首先我们需要清楚 Delphi 中“对象方法(Method)”和“函数/过程(Function/Procedure)”之间的区别。对象方法在调用时会隐藏的传递对象的 Self 指针作为第一个参数,比如我们调用 MainForm.Show(); 实际上在编译后 MainForm 这个对象是作为第一个参数传给 Show 方法了(Delphi 默认的 register 调用方式使用 EAX 寄存器传递)。

而 Delphi 中用 procedure(Sender: TObject) of object; 这种格式声明的 事件(Event) 类型实际上是同时包含有对象和函数的记录。我们可以把一个 TNotifyEvent 的变量强制转换成 TMethod:
TMethod = record
  Code, Data: Pointer;
end;

例如我们声明了一个方法 MainForm.BtnClick 并将它赋值给 btn1.OnClick 事件,实际上是将 MainForm 对象和 BtnClick 方法地址分别作为 TMethod 结构的 Data 和 Code 成员赋值给 btn1.OnClick 事件属性。当 btn1 按钮调用这个 BtnClick 事件时,实际上是将 TMethod 结构的 Data 作为第一个参数去调用 Code 函数。

我们可以编写下面的代码:
procedure MyClick(Self: TObject; Sender: TObject);
begin
  // 第一个参数是虚拟的
  ShowMessage(Format('Self: %d, Sender: %s', [Integer(Self), Sender.ClassName]));
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  M: TMethod;
begin
  M.Code := @MyClick;
  M.Data := Pointer(325); // 随便取的数
  btn1.OnClick := TNotifyEvent(M);
end;
这样就可以将一个普通函数赋值给对象事件属性了。

我们再来看看 TLanguages.Create 的代码:
constructor TLanguages.Create;
type
  TCallbackThunk = packed record
    POPEDX: Byte;
    MOVEAX: Byte;
    SelfPtr: Pointer;
    PUSHEAX: Byte;
    PUSHEDX: Byte;
    JMP: Byte;
    JmpOffset: Integer;
  end;
var
  Callback: TCallbackThunk;
begin
  inherited Create;
  Callback.POPEDX := $5A;
  Callback.MOVEAX := $B8;
  Callback.SelfPtr := Self;
  Callback.PUSHEAX := $50;
  Callback.PUSHEDX := $52;
  Callback.JMP     := $E9;
  Callback.JmpOffset := Integer(@TLanguages.LocalesCallback) - Integer(@Callback.JMP) - 5;
  EnumSystemLocales(TFNLocaleEnumProc(@Callback), LCID_SUPPORTED);
end;

在 Win32 SDK 中可以查到 EnumSystemLocales 要求的回调格式是:
BOOL CALLBACK EnumLocalesProc(
    LPTSTR lpLocaleString         // pointer to locale identifier string
);

而 SysUtils 中的方法声明:
  TLanguages = class
    ...
    function LocalesCallback(LocaleID: PChar): Integer; stdcall;
    ...
  end;

显然,我们是无法将 LocalesCallback 这个方法直接传递给 EnumSystemLocales 的,因为 LocalesCallback 的函数形式声明实际上是:
function LocalesCallback(Self: TLanguages; LocaleID: PChar): Integer; stdcall;
比 EnumLocalesProc 多出来一个参数。

所以在 TLanguages.Create 中,使用了 Callback 结构变量来生成一小段动态代码。这段代码是构造在堆栈中的(局部变量),转换成汇编是:
prcoedure CallbackThunk;
asm
  // 取出 lpLocaleString 参数到 EDX 寄存器
  // CALLBACK EnumLocalesProc 是 stdcall 调用,参数在堆栈中
  POP EDX
  // 将 Self 对象传给 EAX 寄存器
  MOV EAX Self
  // stdcall 调用,将 Self 作为第一个参数压栈
  PUSH EAX
  // 将 lpLocaleString 作为第二个参数压栈
  PUSH EDX
  // 用相对跳转指令跳转到 TLanguages.LocalesCallback 入口地址
  JMP TLanguages.LocalesCallback
end;

将 CallbackThunk 作为临时的回调函数传递给 EnumSystemLocales 是合法的。当回调被执行时,前面那小段代码动态修改了堆栈的内容,将本来只有一个参数的调用,变成了两个参数,从而实现了回调与对象方法的转换。

但是,正如 Passion 在前面提到的,由于这小块临时代码是放在堆栈中的,而 Win2003 的 DEP 限制了在堆栈中执行代码,导致事实上回调函数并没有被正确地调用。

Borland 程序员也看到了这个问题,所以在 BDS 2006 中,这部分代码的实现修改成:
var
  FTempLanguages: TLanguages;

function EnumLocalesCallback(LocaleID: PChar): Integer; stdcall;
begin
  Result := FTempLanguages.LocalesCallback(LocaleID);
end;

constructor TLanguages.Create;
begin
  inherited Create;
  FTempLanguages := Self;
  EnumSystemLocales(@EnumLocalesCallback, LCID_SUPPORTED);
end;
通过声明一个临时变量和转换函数,来取代原来的方法,就不会有 DEP 冲突了。

附带说一下 Forms 单元中的 MakeObjectInstance。这个函数用来生成一块动态代码,将 Windows 的窗体消息处理过程转换为 Delphi 的对象方法调用。在 TWinControl 等需要有消息处理支持的地方用到。该函数也是采用了前面类似的方法,不过不同的是,由于这些转换调用是长期的,所以那些动态生成的代码被放到了标识为可执行的动态空间中了,所以在 Win2003 的 DEP 下仍然可以正常工作:
function MakeObjectInstance(Method: TWndMethod): Pointer;
var
  ...
begin
  if InstFreeList = nil then
  begin
    Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    ...
end;
Author: Passion    Time: 2006-10-13 09:05

写的真详细啊。
Author: VictorWoo    Time: 2006-10-13 13:36

是很透彻的一篇技术帖子。居然也顺带弄清了困扰很久的问题。
这样的现象在.NET下应该不会发生了吧,所有的函数都是类方法或者对象方法。
Author: Passion    Time: 2006-10-13 15:12

例如我们声明了一个方法 MainForm.BtnClick 并将它赋值给 btn1.OnClick 事件,实际上是将 MainForm 对象和 BtnClick 方法地址分别作为 TMethod 结构的 Data 和 Code 成员赋值给 btn1.OnClick 事件属性。“当 btn1 按钮调用这个 BtnClick 事件时,实际上是将 TMethod 结构的 Data 作为第一个参数去调用 Code 函数。”

这里关于调用的似乎值得讨论一下。记得这个事件OnClick在被调用时是这么写的:

if Assigned(FOnClick) then
  FOnClick(Self);

第一个参数是调用时传入的是Button自身,也就是Button的Self,而不是原本这个Method里头的Data吧?
我的理解是,Method的Data只是用来说明这个方法属于哪个对象实例,但被调的时候似乎没发挥作用。所以自行捏造一个TMethod的data部分,然后给OnClick等赋值再调用也能成功。
Author: zjy    Time: 2006-10-13 16:27



QUOTE:
Originally posted by Passion at 2006-10-13 15:12:
例如我们声明了一个方法 MainForm.BtnClick 并将它赋值给 btn1.OnClick 事件,实际上是将 MainForm 对象和 BtnClick 方法地址分别作为 TMethod 结构的 Data 和 Code 成员赋值给 btn1.OnClick 事件属性。“当 btn1 ...

if Assigned(FOnClick) then
  FOnClick(Self);
这里传入的 Self 是 TNotifyEvent 中的 Sender: TObject 参数,而作为对象方法的 OnClick,实际上需要两个参数,第一个隐藏的 Self 是 OnClick 方法所从属的对象,第二个才是 Sender。

比如 Button 调用 FOnClick 时,这个 FOnClick 指向的方法可能是从属于某个 Form 的 OnBtnClick。类自己是不保存对象实例的,直接调用 Form.OnBtnClick 时 Self 是 Form 这个实例,而通过 Button.FOnClick 调用到 Form.OnBtnClick 方法时,OnBtnClick 的 Self 从哪里来?当然就是用 TMethod.Data 传过去的喽。而这个 TMethod.Data 则是在赋值 Button.OnClick := Form.OnBtnClick 时的 Form 对象。
Author: Passion    Time: 2006-10-13 16:54

啊对。FOnClick时传入的Self是作为Sender的,而BtnOnClick方法里头所引用的Self是Form实例,我不小心混了这俩了。后者的Self应该是从Data里头来的。
Author: Alan    Time: 2006-10-13 17:21

Windows 操作系统的DEP确实会导致 TLanguages 出错,前段时间公司项目做多语时在英文XP上做测试时发现DEP会导致 TLanguages 出错,关掉 DEP 就好了,后来在 Borland 网站上找到一个补丁就好了。
Author: kendling    Time: 2006-10-13 17:35

嘿嘿,又学到了不少。
Author: Passion    Time: 2006-10-13 19:42

Alan好久不见了,啥时候出来Happy。




Welcome to CnPack Forum (http://bbs.cnpack.org/) Powered by Discuz! 5.0.0