この記事はWindows Phone Advent Calendar 2014の20日目の記事です。
さて「F#で2ch専用ブラウザをつくろう」では「2ちゃんねる」の掲示板データを読みこむためのライブラリをF#で記述しました。今回はこのライブラリをWindows Phoneアプリの開発から使えるようにしてみます。Windows PhoneアプリにはSilverlightアプリとユニバーサルアプリの2種類がありますが、ここではもちろんユニバーサルアプリを対象にしています。
F#でPortable Class Libraryを作る
F#のコードをユニバーサルアプリから使う基本的な流れは以下のようになります。
- F#のコードをPortable Class Library (PCL)の形でコンパイルしておく
- PCLのアセンブリをユニバーサルアプリのプロジェクトから参照する
PCLを作るにはプロジェクトの新規作成からPortable Libraryを選びます。このプロジェクトテンプレートで生成されるクラスライブラリは.NET 4.5ベースのPCLを生成します。
とりあえず、プロジェクトを生成して、前回作成したコードをインポートしてみます。これで完了!と書きたいところなのですが、実はこのライブラリ、このままでは使えません。
「2ちゃんねる」APIの解説で少し触れましたが、掲示板データのやりとりはShift JISで行います。ところがWindows Phone上のWindows Runtimeではこのエンコーディングはサポートされていないため、Encoding.GetEncoding(“SJIS”)を呼び出した時点で例外が発生してしまうのです。
Shift JISがWindows Phone上のランタイムでサポートされていないのはWindows Phone 7時代からの話なので、実は改めて驚くことはないのですが、PCLでコンパイルが通ったからといって実行できるとは限らない典型例といえます。自分でなんとか実装するしかないのですが、幸い @kazuakix さんによる SJIS Encoding の実装例があるので、そちらを参考にすることにしましょう。
まずはWin32 APIの関数であるMultiByteToWideCharを呼び出すためにWindows Runtimeコンポーネント(SjisEncoding)を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#pragma once #include <ppltasks.h> #define CP_SJIS 932 namespace SjisEncodingComponent { public ref class SjisEncoding sealed { public: static Platform::String^ MultiByteToWideChar(const Platform::Array<byte>^ buff); }; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include "pch.h" #include "SjisEncoding.h" using namespace SjisEncodingComponent; using namespace Platform; Platform::String^ SjisEncoding::MultiByteToWideChar(const Platform::Array<byte>^ buff) { LPCSTR pBuff = (LPCSTR)(buff->Data); if (pBuff == NULL) return ref new Platform::String(); // 空文字を返す const int nSize = ::MultiByteToWideChar(CP_SJIS, 0, pBuff, -1, NULL, 0); BYTE* buffUtf16 = new BYTE[nSize * 2 + 2]; ::MultiByteToWideChar(CP_SJIS, 0, pBuff, -1, (LPWSTR)buffUtf16, nSize); Platform::String^ result = ref new Platform::String((LPWSTR)buffUtf16); delete[] buffUtf16; // *で受けたオブジェクトは従来どおり自前で解放する return result; } |
次にこれをEncodingクラスとして利用できるようにSjisEncodingクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class SjisEncoding : Encoding { public override string WebName { get { return "Shift_JIS"; } } // ShiftJIS バイト列 → Unicode 文字列 public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) { var result = SjisEncodingComponent.SjisEncoding.MultiByteToWideChar(bytes.Skip(byteIndex).Take(byteCount).ToArray()); var idx = 0; foreach (var c in result) { chars[charIndex + idx] = c; idx++; } return result.Length; } // (後略)詳しくは @kazuakix さんの実装をごらんください。 } |
さて、記事にしたがってエンコーディングのクラスを書いてみたのですが、これをF#のコードから使うのにはもうひと工夫必要だったりします。というのも、F#のPCLから参照できるのはPCLのライブラリのみなのですが、このSjisEncodingをPCLのライブラリとしてコンパイルすることはできません。というのもWin32のAPIを呼び出す部分がWinRTコンポーネントになっており、これをPCLから参照することはできないからです。
そこでMonalith.Communicationモジュールから直接このクラスを参照するのではなく、外部からEncodingのインスタンスを受け取るようにして、Monalith.Communicationからユニバーサルアプリプロジェクトへの参照をなくします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
type HttpClient(encoding: Encoding) = let loadAsync parser (uri:Uri) = let toSeq (reader:TextReader) = Seq.initInfinite(fun _ -> reader.ReadLine()) |> Seq.takeWhile(fun line -> line <> null) async { let req = WebRequest.Create(uri) let! res = req.AsyncGetResponse() use stream = res.GetResponseStream() use reader = new StreamReader (stream, encoding) return reader |> toSeq |> Seq.toArray |> parser } member this.getPostsAsync (thread:MoThread) = thread.Uri |> loadAsync Parser.parsePosts member this.getThreadsAsync (board:MoBoard) = board.Uri |> loadAsync (Parser.parseThreads board.Uri) member this.getDefaultCategoriesAsync = Uri "http://menu.2ch.net/bbsmenu.html" |> loadAsync Parser.parseRootMenu |
モジュールとして定義していたHttpClientをクラスとして定義しなおし、Encodingをコンストラクタの引数として受け取るように変更しています。とりあえず、これでWindows Phoneアプリの開発にも使えるPCLとして形なりました。
明日のWindows Phone Advent Calendar 2014 21日目の記事は @karno さんです.
謝辞:快く参照を許可してくれた@kazuakixさん、ありがとうございました。あと、はやくSurface Pro 3買ってください!