20 May 2011

Curious bug in CodeSnip???

A user of my CodeSnip program has reported a strange bug where, after using the Snippets Editor to add a new snippet, the editor dialog box refuses to close, hanging the program.

Apparently this happens only sometimes, and affects v3.8.9 and some earlier versions.

I can't reproduce the problem and was wandering if anyone else has experienced this bug. If so it would be very helpful if you could leave a comment describing what happened and what you were doing when the bug arose.


05 May 2011

Delphi, Javascript and Floating Point Parameters

I recently came across an interesting little problem when using Delphi to call JavaScript in an HTML document loaded into a TWebBrowser control.

The code used the execScript method of the IHTMLWindow interface, which requires that a string containing the JavaScript function call is passed as one of its parameters.

So you have to assemble a string containing the function call and its parameters, something like this:

  JSFn: string;
  StrParam: string;
  IntParam: Integer;
  FloatParam: Double;
  StrParam := 'Say \"Hello\"';  // double quotes must be escaped
  IntParam := 42;
  FloatParam := 1234.56789;
  JSFn := Format('Foo(%d, "%s", %.8f);', [IntParam, StrParam, FloatParam]);

I'll show ExecJS later. It's just a wrapper that ultimately calls IHTMLWindow.execScript with the required JavaScript.

The interesting bit is what happens to the floating point parameter. Delphi consults the current locale when formatting floats, so the decimal separator used in the resulting string depends of the locale. For example, on my UK English system JSFn has value:

Foo(42, "Say \"Hello\"", 1234.56789000);

whereas when I switch to a German locale the value changes to:

Foo(42, "Say \"Hello\"", 1234,56789000);

Now when the function call is passed to the JavaScript engine it has to interpret the string as valid JavaScript. What surprised me at first was the fact that, on the German locale, the float parameter was truncated to an integer value whilst on the UK locale it parsed correctly.

Once I thought about it more it became clear why. When the string containing the function call is passed to the JavaScript engine it has to be parsed. The string must contain valid JavaScript source code. And the JavaScript spec says that decimal numbers use '.' as the decimal separator, which means that any ',' separator is not valid. In fact ',' is the parameter separator so a parameter of '123,4567' is interpreted as two integer parameters: 123 and 4567.

Assume you have an HTML document that contains the following JavaScript in its <head> section:

<script type="text/JavaScript">
  function showNum(n1, n2) {
      'n1 = ' + n1 + '\n\n' + 'n2 = ' + n2

You also have a Delphi program that loads the HTML document into a TWebBrowser control and has the following code attached to a button click:

  Format('showNum(%.8f, %.8f)', [123.456, -42.56])

Running the Delphi program with a UK English locale will work as expected and you see the JavaScript alert box displaying this text:

n1 = 123.456
n2 = -42.56

which is what you expect. Switch to a German locale and run again and you'll get:

n1 = 123
n2 = 45600000

Luckily the fix is simple. We simply get information about the current decimal separator and change it to a '.'.

Here's my first attempt:

function JSFloat(const F: Double): string;
  Result := FloatToStr(F);
  if DecimalSeparator <> '.' then
    Result := StringReplace(Result, DecimalSeparator, '.', [rfReplaceAll]);

This uses the DecimalSeparator global variable that Delphi sets correctly for the current locale when the program starts.

When we change the code that calls the JavaScript function to the following, everything works no matter what the locale.

  Format('showNum(%s, %s)', [JSFloat(123.456), JSFloat(-42.56)])

Using DecimalSeparator is not thread safe, and I think the code is rather clunky, so here's another, thread safe, version of JSFloat that I prefer:

function JSFloat(const F: Double): string;
  FS: TFormatSettings;
  GetLocaleFormatSettings(GetThreadLocale, FS);
  FS.DecimalSeparator := '.';
  Result := FloatToStr(F, FS);

This gets the format settings for the current thread's locale and then replaces the decimal separator with the required '.'. It uses the extended version of FloatToStr to convert the float according the provided locale.

Note that this code doesn't change the locale's decimal separator because we're operating on a copy of the locale information. Therefore we won't trample on any other parts of the program that may need the the correct decimal separator for the locale.

One further tweak suggests itself that takes advantage of the fact that the Format routine can take an optional TFormatSettings parameter:

  FS: TFormatSettings;
  GetLocaleFormatSettings(GetThreadLocale, FS);
  FS.DecimalSeparator := '.';
    Format('showNum(%.8f, %.8f);', [123.456, -42.56], FS)

And finally, the promised implementation of the ExecJS method. Note that this is a method of the form that contains the browser control, which we assume is named WebBrowser1.

procedure TForm1.ExecJS(const Fn: string);
  Doc: IHTMLDocument2;      // current HTML document
  HTMLWindow: IHTMLWindow2; // parent window of current HTML document
  // Get reference to current document
  Doc := WebBrowser1.Document as IHTMLDocument2;
  // Get parent window of current document
  HTMLWindow := Doc.parentWindow;
  // Run JavaScript
  HTMLWindow.execScript(Fn, 'JavaScript') // execute function