gettext-rs icon indicating copy to clipboard operation
gettext-rs copied to clipboard

Strange behavoir with GTK3 (msys64) on Windows

Open art-den opened this issue 3 years ago • 11 comments

gettext works very strange if gtk-rs is used in application. I reproduce it with simple code

use {gtk::*, gtk::prelude::*};
use gettextrs::*;

fn main() {
    TextDomain::new("electra_stacking_gui")
        .prepend(r"D:\rust\electra_stacking\target\debug\share")
        .locale("en_US.UTF-8")
        .init()
        .unwrap();
    let text = gettext("test");
    println!("text = {}", text);

 // uncomment it and code above will show wrong result 
 // (will print `test` instead of translated string)
/*    let application = Application::new(
        Some("com.github.testtest"),
        Default::default(),
    );
    application.run(); */
}
[dependencies]
gtk = "0.15"
gettext-rs = { version = "0.7", features = ["gettext-system"] }

Why does gtk-rs affect gettext behavior?

art-den avatar May 16 '22 19:05 art-den

I suspect that Application::new calls settextdomain, overriding the results of TextDomain::new. You could swap the two calls to fix this, or find out if GTK lets you configure the domain name somehow so you can do away with TextDomain.

Minoru avatar May 16 '22 20:05 Minoru

But TextDomain::init and gettext and are called before Application::new in my example

art-den avatar May 17 '22 03:05 art-den

Too much magic here ))

I redid example like

    TextDomain::new("electra_stacking_gui")
        .prepend(r"D:\test_mo\share")
        .locale_category(LocaleCategory::LcAll)
        .skip_system_data_paths()
        .locale("en_US.UTF-8")
        .init()
        .unwrap();
    let text = gettext("test");
    println!("text = {}", text);
... GTK code here

and started the process monitor to see which .mo files are accessed by my application. The magic is sometimes I see right translated string. I.e. every time I ran my application sometimes I see the correct result and sometimes the wrong result O_o But if to stop process monitor I saw only wrong results O_o

When result is correct accessed .mo-files are (from process monitor):

D:\test_mo\share\locale\en\LC_MESSAGES\electra_stacking_gui.mo    SUCCESS	
D:\test_mo\share\locale\ru_RU\LC_MESSAGES\electra_stacking_gui.mo PATH NOT FOUND
D:\test_mo\share\locale\ru\LC_MESSAGES\electra_stacking_gui.mo    SUCCESS

and when wrong:

D:\mingw64\share\locale\ru_RU\LC_MESSAGES\messages.mo                               PATH NOT FOUND	
D:\mingw64\share\locale\ru\LC_MESSAGES\messages.mo                                  PATH NOT FOUND	
E:\Development\vscode\tools\msys64\mingw64\share\locale\ru_RU\LC_MESSAGES\glib20.mo PATH NOT FOUND
E:\Development\vscode\tools\msys64\mingw64\share\locale\ru\LC_MESSAGES\glib20.mo    PATH NOT FOUND
D:\test_mo\share\locale\en\LC_MESSAGES\electra_stacking_gui.mo                      SUCCESS

Mo-files

D:\test_mo\share\locale\en\LC_MESSAGES\electra_stacking_gui.mo
D:\test_mo\share\locale\ru\LC_MESSAGES\electra_stacking_gui.mo

are equal and compiled from po-source

msgid "test"
msgstr "test test"

So It looks like UB in msys gettext or in gettext-rs

art-den avatar May 17 '22 06:05 art-den

I write simple C test

#include <gtk/gtk.h>
#include <stdio.h>
#include <locale.h>
#include <libintl.h>

int main(int argc, char *argv[]) 
{
    setlocale(LC_ALL, "en_EN.UTF-8");
    bindtextdomain ("electra_stacking_gui", "D:\\test_mo\\share\\locale");
    bind_textdomain_codeset("electra_stacking_gui", "UTF-8");
    textdomain("electra_stacking_gui");
    printf("text = %s\n", gettext("test"));

    GtkApplication *app = gtk_application_new(
        "org.gtk.example.HelloApp", 
        G_APPLICATION_FLAGS_NONE
    );
    int status = g_application_run(G_APPLICATION(app), argc, argv);
    g_object_unref(app);
    return status;
}

This example is ok

art-den avatar May 17 '22 08:05 art-den

More investigation. Working examples: (1)

fn main() -> anyhow::Result<()> {
    setlocale(LocaleCategory::LcAll, "");
    bindtextdomain("electra_stacking_gui", r"D:\test_mo\share\locale")?;
    textdomain("electra_stacking_gui")?;

    let text = gettext("test");
    println!("before Application::new = {}", text);

    let application = Application::new(Some("com.github.testtest"), Default::default(),);
    application.run();

    Ok(())
}

(2)

fn main() -> anyhow::Result<()> {
    setlocale(LocaleCategory::LcAll, "");
    bindtextdomain("electra_stacking_gui", r"D:\test_mo\share\locale")?;
    textdomain("electra_stacking_gui")?;

    // Sleep added after `gettext` configuration is Ok
    std::thread::sleep(std::time::Duration::from_millis(100));

    let text = gettext("test");
    println!("before Application::new = {}", text);

    let application = Application::new(Some("com.github.testtest"), Default::default());
    application.run();

    Ok(())
}

Not working examples: (1)

fn main() -> anyhow::Result<()> {
    // OMG! This sleep breaks gettext!!!
    std::thread::sleep(std::time::Duration::from_millis(100));

    setlocale(LocaleCategory::LcAll, "");
    bindtextdomain("electra_stacking_gui", r"D:\test_mo\share\locale")?;
    textdomain("electra_stacking_gui")?;

    let text = gettext("test");
    println!("before Application::new = {}", text);

    let application = Application::new(Some("com.github.testtest"), Default::default());
    application.run();

    Ok(())
}

(2)

fn main() -> anyhow::Result<()> {
    let application = Application::new(Some("com.github.testtest"), Default::default());

    // Placing gettext configuration after `Application::new`
    // brakes gettext
    setlocale(LocaleCategory::LcAll, "");
    bindtextdomain("electra_stacking_gui", r"D:\test_mo\share\locale")?;
    textdomain("electra_stacking_gui")?;

    let text = gettext("test");
    println!("before Application::new = {}", text);

    application.run();

    Ok(())
}

@Minoru, any ideas?

art-den avatar May 17 '22 15:05 art-den

FWIW, all the provided examples work for me on Linux. (This one contains a typo, should say "en_US.UTF-8" rather than "en_EN.UTF-8".) I don't have a Windows machine to test this, nor do I really have interest in digging deeper into that OS.

The fact that it looks for glib20.mo, and also that it works occasionally or doesn't work with a delay, gives me an idea that it's a race when loading DLLs: intl.dll from miingw for gettext, and glib20.dll for GTK (the DLL names are guesses).

How do you compile and link your C example? It's surprising that it works fine. I bet you're linking libintl statically! :)

Minoru avatar May 19 '22 20:05 Minoru

Hi, I changed en_EN.UTF-8 -> en_US.UTF-8 in my C example and still no problem there. Also I added sleep(1); before gettext configuration, and no change.

I build C example with script

md bin
gcc -g0 -O3 -s -o bin/hello_world.exe ^
    main.c ^
    %msys%\mingw64\lib\libgtk-3.dll.a ^
    %msys%\mingw64\lib\libgobject-2.0.dll.a ^
    %msys%\mingw64\lib\libgio-2.0.dll.a ^
    %msys%\mingw64\lib\libglib-2.0.dll.a ^
    %msys%\mingw64\lib\libintl.dll.a

Look at last library. It called libintl.dll.a and it is for dynamic linking. I check idata section of example binary and it contains import of libintl-8.dll and list of functions:

libintl_bind_textdomain_codeset
libintl_bindtextdomain
libintl_gettext
libintl_setlocale
libintl_textdomain

I already translated my application and test it in different windows versions. Time by time I see untranslated interface if to start (and then close) application many times. So I need Ideas where to look to sort out the situation

art-den avatar May 20 '22 03:05 art-den

Got it. Since the C program doesn't misbehave, I'm convinced that gettext-sys or gettext-rs is at fault here, but I don't see how. I'd try a debugger to see what exactly gets called when you invoke gettext; I still suspect that in some cases it calls libintl while in other it calls libglib. Sorry I don't have any better ideas.

Minoru avatar May 20 '22 19:05 Minoru

Hm... If to set breakpoint at line with bindtextdomain in rust example sometimes I see stacktrace of one parallel thread

RtlCompareMemoryUlong (@RtlCompareMemoryUlong:9)
RtlGetCurrentServiceSessionId (@RtlGetCurrentServiceSessionId:1905)
RtlGetCurrentServiceSessionId (@RtlGetCurrentServiceSessionId:1286)
RtlFreeHeap (@RtlFreeHeap:24)
RtlRegisterSecureMemoryCacheCallback (@RtlRegisterSecureMemoryCacheCallback:1769)
RtlGetCurrentServiceSessionId (@RtlGetCurrentServiceSessionId:1380)
RtlGetCurrentServiceSessionId (@RtlGetCurrentServiceSessionId:1286)
RtlFreeHeap (@RtlFreeHeap:24)
free (@free:11)
g_getenv (@g_getenv:130)
g_win32_getlocale (@g_win32_getlocale:16)
g_get_current_dir_utf8 (@g_get_current_dir_utf8:45)
glib_gettext (@glib_gettext:8)
g_tcp_wrapper_connection_get_base_io_stream (@g_tcp_wrapper_connection_get_base_io_stream:118)
g_type_class_ref (@g_type_class_ref:308)
g_object_new_valist (@g_object_new_valist:307)
g_object_new (@g_object_new:11)
g_themed_icon_new (@g_themed_icon_new:17)
g_list_store_find (@g_list_store_find:4718)
g_list_store_find (@g_list_store_find:4793)
g_list_store_find (@g_list_store_find:6057)
g_list_store_find (@g_list_store_find:6512)
g_get_num_processors (@g_get_num_processors:207)
g_test_get_path (@g_test_get_path:30)
g_private_replace (@g_private_replace:79)
_beginthreadex (@_beginthreadex:85)
_endthreadex (@_endthreadex:53)
BaseThreadInitThunk (@BaseThreadInitThunk:9)
RtlUserThreadStart (@RtlUserThreadStart:12)

I'm not sure it fully correct but glib_gettext item can give you some ideas?

art-den avatar May 20 '22 21:05 art-den

Sorry I'm so slow to respond! I'm kind of swamped lately.

That stacktrace is in line with my hypothesis about a race between DLLs. I have no idea how to debug it further though ._.

Minoru avatar May 26 '22 19:05 Minoru

No problem. I'll look into this problem in more depth after I've finished some ideas for my application

art-den avatar Jun 01 '22 18:06 art-den