none
DLLとのやりとりにShift-JISを使用する時、文字が切れる RRS feed

  • 質問

  • Declare Sub RtlMoveMemory Lib "kernel32" (dst As String, src As String, size As Integer) Sub TestMoveMeory() Dim dst As String = "字BC" Dim src As String = "字A" RtlMoveMemory(dst, src, 3) Debug.Print(dst) ' "字A"になる、"字AC"としたい Debug.Print(src) ' "字"になる、"字A"としたい End Sub

    上記コードを実行すると、一文字切れます。良い方法はありますか?


    • 編集済み nwpfh 2015年9月21日 23:06
    2015年9月21日 23:05

回答

  • ここで挙げられている話かな?
    2018 年登録で open のまま進行していないので、現状はすぐに変わらないでしょう。

    https://github.com/dotnet/coreclr/issues/16971

    個人的には元のお題の出し方が迷走を呼ぶコードだったのだと思いますが、事象自体は CLR の不具合なので、コードを変更して回避する以外の手立てはなさそうな感じです。

    • 回答としてマーク nwpfh 2019年1月26日 23:20
    2019年1月26日 22:58
    モデレータ

すべての返信

  • Shift-JISとか関係ありません。RtlMoveMemoryの第3引数の文字数ではなくバイト数であり、渡している値の単位が間違っています。

    もしWindows APIで文字操作を行いたいのであればShell String Handling Functionsの中からStrCpyN等を使うことです。(追記。それでもShift-JISでの文字数を数えないとずれます。)ただしその場合でも.NETにおけるString型の値を変更してはいけません。文字列に対する既定のマーシャリングの固定長の文字列バッファーでも説明されていますが、StringBuilderを使う必要があります。

    とはいえ、.NET上ではUnicodeで保持されている文字列を一旦Shift-JISに変換し外部DLLで処理した後、さらに得られた結果をUnicodeへ戻すのは不毛です。


    • 編集済み 佐祐理 2015年9月22日 0:20
    • 回答の候補に設定 星 睦美 2015年9月24日 5:29
    2015年9月22日 0:17
  • 質問者さんはどのようなアプリを作っていて、どういうことがしたいのですか?

    部分的な質問だけでなく全体的に何をしたいのかを書いた方がより有益な情報(例えば、やりたいことはこうすれば実現できるなど)が得られると思いますけど・・・。


    #あと、環境なども書くようにしましょう。面倒だと思っているかもしれませんが、適切に情報が提供されていれば、回答者が質問者さんの状況を的確に把握でき、タイムリーで的を得た回答が得られるということで、質問者さんにもメリットがあります。質問の仕方に関するガイドラインも出ていますので目を通していただければと思います。
     
    フォーラムのご利用方法(質問の投稿)について
    https://social.technet.microsoft.com/Forums/ja-JP/home?forum=announceja&announcementId=587d27f8-adc8-432a-905c-81375f8a05ec


    • 編集済み SurferOnWww 2015年9月22日 1:23 脱字追加
    2015年9月22日 0:46
  • 佐祐理さん、こんにちは。

    主旨は、ほぼ分かるのですが、第3引数がバイト数ならば、Shift-JISでは、"字A"は、3バイトになり、形式的には、意図している事は分かります。(実際、Shift-JISとは思えませんが)  ただ、RtlMoveMemory の dstと src に Stringを渡した場合の動作はどうなるのでしょう?
    こちらの動きが良く分かりません。 nwpfhさんの結果では、変更されないと思われる srcの値も変わっているように見えますが、何が起きているのでしょう?

    元々の使い方に問題があるとは思いますが、少し、気になりましたので、書かせていただきます。

    2015年9月22日 10:07
  • あー、元のコードは、推奨されない互換記法ですが、「問題がある」とまでは言えず、理屈上はきちんと動いくはずです。(し、文字をASCIIのみで構成してやれば実際想定通りに動きます。)
    VBのdeclare文で引数での裸stringは、VBByRefStrであり「書き込みが反映されるstring」として扱われます。で、呼び出しの際には、いわゆるUNI->ANSI変換(と逆のANSI->UNI変換)も行われます。要するにVB6とかと同じ動作、というわけですね(そういえばVB6もstringは内部UNICODEで、DLL呼び出しの際は変換がかかる仕様でしたか)。

    一方、漢字を含んだままでは、RtlMemCpyのsizeを0にしたり、代わりにMessageBoxを呼んだりしてもsrcとdstが同じように破損する(上、32bitと6bitで破損の仕方が異なる)ので、まーこの文字コード変換のあたりのバグなんでしょうねー。二回目のMesageBoxがチビれるのはちょっと笑いましたね、、

    Declare文なんか止めて、DllImportつきのFunction宣言にするのが本邦においては解決方法となるでしょう。もちろん、書き込まれる文字列はStringBuilderで宣言するとか、開き直ってByte型の配列にするとかの考慮は要りますがネ。

    Declare Function MBox Lib "user32.dll" Alias "MessageBoxA" (ByVal hWnd As IntPtr, ByVal txt As String, ByVal caption As String, ByVal Typ As Integer) As Integer Sub TestMoveMeory() Dim dst As String = "字BC" Dim src As String = "字A" MBox(IntPtr.Zero, dst, src, 0) Debug.Print(dst) ' x86:"字B\0", x64:"字B" Debug.Print(src) ' x86:"字\0", x64:"字"

    MBox(IntPtr.Zero, dst, src, 0) End Sub




    jzkey

    2015年9月24日 14:07
  • 返信ありがとうございます。

    渡している単位はあっています。Shift-JISで3バイト。

    それが仕様です。

    2016年12月21日 21:40
  • 返信ありがとうございます。

    既存の資産があるのでなんともです。

    バグはとってくる値間違いですね。

    Unicode 3文字をSjis 4バイトで渡して、Sjis3文字を受け取ってUnicode2文字にしてるっぽいです。

    Unicode 3文字をSjis 4バイトで渡して、Sjis4バイトを受け取ってUnicode3文字にする必要があるのにです。。。

    2016年12月21日 21:45
  • 返信ありがとうございます。

    ロジックは下に書いてます、興味があれば。

    2016年12月21日 21:47
  • Import System.Rumtime.InterropService
    Declare ... (<MarshalAs(UnmanagedType.LPTStr)> dst, 略
    書き込み環境の問題で簡素でごめん。型を教えてやれば、一応、期待する結果になる、と思う。

    Jitta@わんくま同盟

    2016年12月26日 0:52
  • Import System.Rumtime.InterropService
    Declare ... (<MarshalAs(UnmanagedType.LPTStr)> dst, 略
    書き込み環境の問題で簡素でごめん。型を教えてやれば、一応、期待する結果になる、と思う。

    文字列を API に引き渡す場合の指定方法について、よねKENさんのサイトで詳しく説明されていました。

    GetPrivateProfileStringにみるAPIの使用方法

    GetPrivateProfileString関数の様々な宣言法によるWin2000、Win98での実行結果の成否

    2016年12月27日 10:23
  • 渡している単位はあっています。Shift-JISで3バイト。


    API 宣言時の Unicode/ANSI/Auto の違いが重要ですね。

    元質問は、dst = "字BC" の先頭 3 バイトを
    src の先頭 3 バイトで置き換えるコードになっていますが、
    src の内容によって、下記のような差異が生じます。

    今回の例だとパターンAなので、Unicode/ANSI の差が出にくいですが、
    それが本当に期待する結果なのかは、元質問からは読み取れません。


    【パターンA】
    src = "字A" の 先頭 3 バイトを複写する場合、
    Shift_JIS だと「字AC」という結果になりますし、
    Unicode でも「字AC」で同じ結果になります。


    【パターンB】
    src = "A字" の 先頭 3 バイトを複写する場合、
    Shift_JIS だと「A字C」になりますが、
    Unicode だと 2 文字目が破損してしまいます。


    【パターンC】
    src = "㎥/h" の 先頭 3 バイトを複写する場合、
    Shift_JIS だと 1 文字目が破損しますが
    Unicode なら「㎥/C」と化けずに置き換わります。


    Ansi モードで呼び出す場合、出力側は String ではなく StringBuilder を使う事になります。
    具体的には下記のようにします。Declare でも DllImport でも構いません。

    Declare Ansi Sub RtlMoveMemory Lib "kernel32" (dst As StringBuilder, <MarshalAs(UnmanagedType.LPStr)> src As String, size As Integer)
    
    <DllImport("kernel32", Charset:=CharSet.Ansi)> Sub RtlMoveMemory(<MarshalAs(UnmanagedType.LPStr)> dst As StringBuilder, src As String, size As Integer)
    End Sub
    

    この手法はAおよびBのパターンに対して用いられます。
    dst が String ではなくなるので、呼び出し側の変数を修正する必要がありますね。

    どうしても dst を String にしたままにするのなら、Unicode 呼び出しになると思います。
    この場合、AおよびCのパターンが対象になります。

    この条件を満たすような宣言の例を幾つか挙げておきます。
    (Windows 9x 系では未検証)

    '1: DllImport(Auto) + 属性指定なし
    <DllImport("kernel32", Charset:=CharSet.Unicode)> Sub RtlMoveMemory(dst As String, src As String, size As Integer)
    End Sub
    
    ’2: Declare Unicode + 属性指定なし
    Declare Unicode Sub RtlMoveMemory Lib "kernel32" (dst As String, src As String, size As Integer)
    
    ’3: Declare Unicode + LPWStr
    Declare Unicode Sub RtlMoveMemory Lib "kernel32" (<MarshalAs(UnmanagedType.LPWStr)> dst As String, <MarshalAs(UnmanagedType.LPWStr)> src As String, size As Integer)
    
    ’4: DllImport(Auto) + 属性指定なし
    <DllImport("kernel32", Charset:=CharSet.Auto)> Sub RtlMoveMemory(dst As String, src As String, size As Integer)
    End Sub
    
    ' 5: Declare Auto + 属性指定なし
    Declare Auto Sub RtlMoveMemory Lib "kernel32" (dst As String, src As String, size As Integer)
    
    ’6: Declare Auto + LPTStr
    Declare Auto Sub RtlMoveMemory Lib "kernel32" (<MarshalAs(UnmanagedType.LPTStr)> dst As String, <MarshalAs(UnmanagedType.LPTStr)> src As String, size As Integer)


    上記はいずれも「dst は "字AC" に」「src は "字A" に」という両方の条件を満たします。

    しかし、下記のように記述してみると、2 と 5 は b = False、それ以外は b = True となるのでご注意下さい。
    そのため個人的には、 2 や 5 の記法はあまりお奨めしません。

    Dim src As String = "字A"
    Dim src2 As String = src
    RtlMoveMemory(dst, src, 3)
    Dim b As Boolean = src Is src2

    2016年12月27日 12:22
  • 返信ありがとうございます。

    どうも、質問内容を読んでもらえてないようです。

    Shift-JISで3バイトコピーする話です。
    Unicodeであれば問題ないという回答が複数見受けられます。

    英語しか話せない友人がいるんだけど、英語翻訳機の性能が悪くて通じませんどうしたら良いでしょうか?
    日本語で会話すれば問題ないでしょ?と言われているように感じます。

    そもそも、Unicodeの話なら文字列処理そのままで良いと思います。

    Unicodeを3バイトコピーした結果が、たまたま期待値と一致しようが、
    そんな要件が必要になることはないのではないでしょうか。

    例えば、Unicode(UTF16LE)では"AB"は4バイトであり、3バイトコピーしても期待値と同じ結果にたまたまなるケースがあることに何の意味があるのでしょうか。
    A(&H41 + &H00) + B(&H42 + &H00)

    話題はすべて「ANSI」一択の話です。Shijft-JISというのは、日本語OSだとANSIでDLLをコールすると
    UnicodeがShift-JISにマーシャリングされ、DLLの関数実行後、出力項目についてはShift-JISからUnicodeにアンマーシャリングされます。

    まず、この動きを知らない人は回答しないでいただきたいです。






    • 編集済み nwpfh 2019年1月26日 7:36
    2019年1月26日 7:01
  • Stringオブジェクトはオブジェクトであり、C言語で文字列を表現する時に使用するchar*とは異なりただのメモリブロックではないものなので、CopyMemoryは想定外の動作をしうる、という以上のことはないと思います。
    2019年1月26日 8:52
  • Hongliangさんよりもうちょっと直接的に。

    Visual Basic .NETおよびそれが動作している.NET FrameworkのString型には

    The value of the String object is the content of the sequential collection of System.Char objects, and that value is immutable (that is, it is read-only).

    と説明されているように、不変です。外部から変更すべきでもありません。変更したいのであればStringBuilderを使用すべきです。

    2019年1月26日 8:57
  • Stringが書き込み禁止なのでネイティブ側にポインタを渡して書き込みさせると問題、というのはその通りですが、今回問題になっているのは「VBByRefStrのマーシャリングって日本語渡すと動作がおかしくね?」って話ですよ。

    ネイティブ呼び出しがStringに書き込んでいるのがご不満であれば、下記のコードについて、如何お考えでしょう。

    <DllImport("user32.dll", SetLastError:=true, CharSet:=CharSet.Ansi)> _
    Public Function MessageBox (ByVal hWnd As IntPtr, <MarshalAs(UnmanagedType.VBByRefStr)> byref txt As String, ByVal caption As String, ByVal Typ As Integer) As Integer
    End function
    
        <STAThread()> _
        Sub Main()
            Dim dst As String = "字BC"
            MessageBox(IntPtr.Zero, dst, dst, 0)
            System.Console.WriteLine(dst) ' x86:"字B\0", x64:"字B"
            MessageBox(IntPtr.Zero, dst, dst, 0)
        End Sub
    End Module
    APIについては、下記Declare文でも再現します。
        Declare Function MessageBox Lib "user32.dll" Alias "MessageBoxA" (ByVal hWnd As IntPtr, ByVal txt As String, ByVal caption As String, ByVal Typ As Integer) As Integer


    jzkey

    2019年1月26日 12:14
  • 返信ありがとうございます。

    唯一、jzkeyさんのみが同じレベルでお話していただいているようです。

    Sub ChangeString(ByRef aValue As String)
    
        aValue = aValue & "is changed."
    
    End Sub

    上記関数のように、String型がimmutableであることと、ポインタのポインタ渡し(char **)で引数が渡されたとき、ポインタのポインタの参照先が変更されることは矛盾しませんよ。

    「<MarshalAs(UnmanagedType.VBByRefStr)>」が暗黙的に付くことで、ポインタのポインタ渡しになっているに過ぎません。
    String型の参照先を変えることは禁止されていますが、ポインタ型(String *)の値を変更することは禁止されていませんよ。


    • 編集済み nwpfh 2019年1月26日 15:45
    2019年1月26日 15:34
  • ここで挙げられている話かな?
    2018 年登録で open のまま進行していないので、現状はすぐに変わらないでしょう。

    https://github.com/dotnet/coreclr/issues/16971

    個人的には元のお題の出し方が迷走を呼ぶコードだったのだと思いますが、事象自体は CLR の不具合なので、コードを変更して回避する以外の手立てはなさそうな感じです。

    • 回答としてマーク nwpfh 2019年1月26日 23:20
    2019年1月26日 22:58
    モデレータ
  • 返信ありがとうございます。

    まあ、タイトルに「DLLとのやりとりにShift-JISを使用する時、文字が切れる」と書いているが本題に書いていないのがまずかったかもしれないですね。
    タイトルを読んで、読み間違えることはまずないはずです。

    また、当件はGitHubになるまえにMicrosoft Connect時代にバグレポートを書いてマイクロソフトからレビューする等、返事をいただいたきりうやむやになってたので、改めて今のフィードバックチームに問い合わせています。

    迷走しているのは日本語翻訳が「String」を「文字列」、「Reference」を「参照」と変換しているところにあるように感じます

    https://docs.microsoft.com/ja-jp/dotnet/framework/interop/default-marshaling-for-strings#cpcondefaultmarshalingforstringsanchor3

    常に、「ただし、StringBuilderの場合、(*BStrとVBByRefStrを除く)LP*Strのみに対応する」と書かれており、Stringはすべて対応できるが、StringBuilderは限定対応のみと書かれているだけです。
    ドキュメント上、「String」が「文字列」と変換されるため、もしかしたらStringBuilderのみが有効と読み取る人もいるかもしれません。

    実際は「Even if the string is passed by reference, there is no way to initialize the buffer to a given size.」LP*Str及びVBByRefStrでバッファ長を指定してString型を渡すことはできないと書かれているだけです。
    ※LP*StrをString型で使用した場合の動作の記憶が曖昧なので、位置づけが間違っているかもしれません。

    LP*Strの場合、値もバッファ長も変更不可。VBByRefStrでさえ、バッファ長を指定することはできない。(つまり、VBByRefStrであれば値の変更は可能)

    これも「reference」ではなく「参照」と書くことで「書き込み禁止」を指しているように感じる人もいることでしょう。

    議論が嫌いな訳ではないので、みなさんとご意見をかわし楽しかったです。貴重な時間を割いていただき、ありがとうございました。





    • 編集済み nwpfh 2019年1月27日 3:04
    2019年1月27日 0:08
  • このコメントで初めてVBByRefStrの存在を知りました。私はこれを知らずにコメントしていました、すみません。

    なお、Microsoft ConnectからGithubに移行したわけではありません。Azuleanさんが挙げられたIssueはcoreclr、つまり.NET Coreに関するものであり、.NET Frameworkとは独立して管理されています。

    2019年1月27日 0:56
  • あら、そうなんですね。

    昔のマイクロソフトからの返信メールのリンクに飛んだら「Microsoft Connect Retired」と出てきたのでAzuleanさんのをみてそっちにいったのかと思ってました。

    まあ、さすがに各社のマイグレーションも終わっているとは思うので、今更感がある話ではあります。
    この対応に本当に泣かされた。。。

    2019年1月27日 1:03
  • .NET Framework/Visual Studio の Connect の後継は Developer Community ですね。

    // .NET Framework と .NET Core の将来に対する Microsoft のスタンスは .NET チームのブログ に書かれています。
    // 興味があればどうぞ。

    2019年1月27日 11:52
    モデレータ