20201122のC#に関する記事は9件です。

Unityから考えるDI

はじめに

 Zenject/Extenjectを使った方がなんか良いらしい。じゃぁ使い方を調べよう。そんな具合でそこから入ったものの、だいぶ飲み込むのに苦労した経験があります。特にZenject/Extenjectは多機能ですので、余計に要点を把握しにくいのではないかと思う所があります。
 ですんで、そういったものを使う前の具体的な話からしていって、意義を書き留めておきたいなと思います。

用語整理

DI

 Dependency Injection。依存性の注入。詳細は後述。

DIコンテナ

 DIをしてくれるフレームワーク。Zenject/Extenject、VContainer等。

Zenject/Extenject

 DIをしてくれるフレームワークの一種。基本的にZenjectもExtenjectも同じものだが、政治的要因でZenjectからExtenjectが分離した歴史を持つ。現在はExtenjectが本流だが、そういう経緯からExtenjectのことも含めてZenjectと呼称することが多い。本稿ではZenject/Extenjectと表記する。

VContainer

 DIをしてくれるフレームワークの一種。最近リリースされた。Zenject/Extenjectよりもシンプルで軽いらしい。

今実装すること、将来実装すること

 ゲーム制作も順調です。次のことを実装しようと思います。

  • ハイスコアの記録
  • 将来的にはサーバーに保存してスコアランキングを表示する

 サーバーはまだ選定すら済んでいません。とりあえず、PlayerPrefsを使って保存することにしましょう。1ゲーム終了時にはGameControllerクラスのOnFinishedが実行されるとします。まずはこんなコードになるでしょう。

GameController.cs
using  UnityEngine;

public class GameController
{

    ・・・

    private void OnFinished(int score, int currentHighScore)
    {
        if (score < currentHighScore) return;
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save();
    }
}

 慣れた方ならGameControllerというネーミングに警戒心を覚えるでしょう。しかもそこに保存するコードを直接書くなんて!実際にここは今後書き換える可能性が非常に高いです。将来的にサーバーに保存したい所ですから。ハイスコアの保存に関することは一つのクラスにまとめておきましょう(単一責任の原則)。

HighScoreRepository.cs
using UnityEngine;

public class HighScoreRepository
{
    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save();
        return true;
    }
}
GameController.cs
public class GameController
{
    private readonly HighScoreRepository _highScore = new HighScoreRepository();

    ・・・

    public void OnFinished(int score, int currentHighScore)
    {
        _highScore.Save(score, currentHighScore);
    }
}

 Repositoryというのは永久保存する場所という意味です。HDDやサーバーなどです。これで少しは安眠できるでしょう。

依存とその問題

 サーバーが決まり、契約も済ませました。いつでも使える状態です。じゃぁHighScoreRepositoryを書き換えましょう。ただ、開発中はPlayerPrefsの方が便利なので、前のコードも残しておきたい所です。コメントアウトしておきましょうかね。コメントアウトする場所をちょっと弄れば、切り替えられます。

HighScoreRepository.cs
using UnityEngine;

///*
public class HighScoreRepository
{
    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        //サーバー保存処理
        return true;
    }
}
//*/

/*
public class HighScoreRepository
{
    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save();
        return true;
    }
}
//*/

 ちょっと不細工ですね。GameControllerを弄って、if文で切り替えられるようにしましょうか?それもまたリスクがあります。今はOnFinishedだけですが、今後他の場所でHighScoreRepository.Saveを実行する可能性はないでしょうか?そこでちゃんと忘れずにifで切り替えられるでしょうか?怖いですね。
 GameControllerがHighScoreRepositoryに依存している状態です。HighScoreRepositoryやその周辺をいじるときは、GameControllerもうまく歩調を合わせてやらないといけません。面倒ですね。

DI:依存性の注入

 ここで出てきます。依存性の注入。GameControllerでは依存するHighScoreRepositoryを次のように書いていました。

private readonly HighScoreRepository _highScore = new HighScoreRepository();

これをGameControllerの外でやります。外で作るだけではあまり状況は変化しないので、HighScoreRepositoryをInterfaceにして、外で切り替えられるようにします。例えばこうします。

IHighScoreRepository.cs
public interface IHighScoreRepository
{
    bool Save(int score, int currentHighScore);
}
HighScoreRepositoryPlayerPrefs.cs
using UnityEngine;

public class HighScoreRepositoryPlayerPrefs : IHighScoreRepository
{
    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        PlayerPrefs.SetInt("Score", score);
        PlayerPrefs.Save();
        return true;
    }
}
HighScoreRepositoryServer.cs
public class HighScoreRepositoryServer : IHighScoreRepository
{
    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        //サーバー保存処理
        return true;
    }
}
GameController.cs
public class GameController
{
    private readonly IHighScoreRepository _highScore;

    public GameController(IHighScoreRepository highScoreRepository)
    {
        _highScore = highScoreRepository;
    }

    public void OnFinished(int score, int currentHighScore)
    {
        _highScore.Save(score, currentHighScore);
    }
}

 HighScoreRepositoryはInterfaceにしました。これは上述通り。で、注目するところはGameControllerのコンストラクタ。ここでIHighScoreRepositoryをもらうようにします。HighScoreRepositoryに依存していたわけですが、これを外から注入、つまり入れてやります。これが依存性の注入です。GameControllerにとってはPlayerPrefsかサーバーかどこかよく分からんけど、とりあえず良い所:heart:に保存される、という認識になります。
 
 したがってGameControllerを作るときはこんな感じになります。

GameControllerLoader.cs
using UnityEngine;

public class GameControllerLoader : MonoBehaviour
{
    private GameController _gameController;
    void Start()
    {
        IHighScoreRepository highScore;
        bool debugMode = true;
        if (debugMode)
        {
            highScore = new HighScoreRepositoryPlayerPrefs();
        }
        else
        {
            highScore = new HighScoreRepositoryServer();
        }
        _gameController = new GameController(highScore);
    }
}

サーバーのみに必要なパラメータ

 そういえば、せっかくハイスコアランキングに登録するのに名前が表示されないなんてちょっと寂しいですね。PlayerPrefsには必要ない要素でしたが。どうしましょうか?
 IHighScoreRepository.Saveの引数に追加するのも一つです。しかし、PlayerPrefsには不要で、サーバー保存時のみに必要です。GameControllerも書き直さなきゃいけません。面倒ですね。じゃぁこうしましょう。

INameGetter.cs
public interface INameGetter
{
    string Get();
}
NameGetterConst.cs
public class NameGetterConst : INameGetter
{
    public string Get()
    {
        return "仕様書無しさん";
    }
}
HighScoreRepositoryServer.cs
public class HighScoreRepositoryServer : IHighScoreRepository
{
    private readonly INameGetter _name;
    public HighScoreRepositoryServer(INameGetter name)
    {
        _name = name;
    }

    public bool Save(int score, int currentHighScore)
    {
        if (score < currentHighScore) return false;
        //サーバー保存処理
        //ここで _name.Get() を使う。
        return true;
    }
}
GameControllerLoader.cs
using UnityEngine;

public class GameControllerLoader : MonoBehaviour
{
    private GameController _gameController;
    void Start()
    {
        IHighScoreRepository highScore;
        bool debugMode = true;
        if (debugMode)
        {
            highScore = new HighScoreRepositoryPlayerPrefs();
        }
        else
        {
            var name = new NameGetterConst();
            highScore = new HighScoreRepositoryServer(name);
        }
        _gameController = new GameController(highScore);
    }
}

 GameControllerを書き換えなくても済みました。HighScoreRepositoryServerの生成時に直接文字列を入れても良いのですが、将来はサーバー保存時に名前入力ダイアログを出したいので、またInterfaceにしました。今はサーバー保存の実装に注力して後で切り替えます。

Loaderの肥大化

 割とシンプルなプログラムですが、GameControllerLoaderは割と大きくなってきます。これからもどんどん大きくなるでしょう。単一責任の原則にしたがってクラスを作っていけば、それなりの数になります。この生成を管理するとなると面倒です。
 はい、お待たせしました。ここで出てくるのがDIコンテナです。

Zenject/Extenjectの場合

 Zenject/Extenjectをインポートした後、SceneContextを作ります。で、MonoInstallerのスクリプトを作成、それを空のGameObjectにアタッチして、SceneContextのMonoInstallerにアタッチします。※この辺りの詳しい利用方法は検索すれば出てくると思います。
 スクリプトは以下のようになります。

Installer.cs
using Zenject;

public class Installer : MonoInstaller
{
    public override void InstallBindings()
    {
        bool debugMode = true;
        if (debugMode)
        {
            Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryPlayerPrefs>().AsSingle();
        }
        else
        {
            Container.Bind<INameGetter>().To<NameGetterConst>().AsSingle();
            Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryServer>().AsSingle();
        }

        Container.Bind<GameController>().AsSingle();
    }
}
GameControllerLoader.cs
using UnityEngine;
using Zenject;

public class GameControllerLoader : MonoBehaviour
{
    [Inject] private GameController _gameController;
}

 先ほどと同じように動くはずです。GameControllerLoader.StartがInstaller.InstallBindingsに移ったような感じです。
 詳しく見ていきましょう。まず_gameControllerに[Inject]という属性がついています。Zenject/Extenjectはこれを探してきます。見つかったら、ここにインスタンスを放り込みます。
 この放り込まれるインスタンスの型は予めZenject/Extenjectに伝えておかねばなりません。それがInstaller.InstallBindingsの

Container.Bind<GameController>().AsSingle();

です。AsSingle()はインスタンスを1個だけ作るという意味です。今回はGameControllerのInjectが1カ所しかありませんが、複数書かれる場合もあります。そのとき、常に同じ一つのインスタンスが挿入される、という意味です。
 このようにZenject/ExtenjectはGameControllerのインスタンスを作ってくれる訳ですが、GameControllerのコンストラクタには引数がありました。IHighScoreRepositoryです。これについても何を挿入したら良いか、予めZenject/Extenjectに伝えておかねばなりません。それが

bool debugMode = true;

if (debugMode)
{
    Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryPlayerPrefs>().AsSingle();
}
else
{
    Container.Bind<INameGetter>().To<NameGetterConst>().AsSingle();
    Container.Bind<IHighScoreRepository>().To<HighScoreRepositoryServer>().AsSingle();
}

です。
 debugMode==trueの時、IHighScoreRepositoryを挿入しなければいけない時はHighScoreRepositoryPlayerPrefsを作ってそれを入れて、という意味になります。
 debugMode==falseだとHighScoreRepositoryServerになりますが、このコンストラクタでさらにINameGetterが要りますので、INameGetterをNameGetterConstに指定します。

VContainerの場合

 まずVContainerをインポートします(manifest.jsonに「"nuget.mono-cecil": "0.1.6-preview"」を追加するのを忘れずに)。で、下記のスクリプトを作って、空のGameObjectにアタッチします。InspectorにGameControllerLoader欄ができてるので、そこに上述のGameControllerLoaderのGameObjectを放り込んでおきます。

GameLifetimeScope.cs
using UnityEngine;
using VContainer;
using VContainer.Unity;

public class GameLifetimeScope  : LifetimeScope
{
    [SerializeField] private GameControllerLoader gameControllerLoader;
    protected override void Configure(IContainerBuilder builder)
    {
        bool debugMode = true;
        if (debugMode)
        {
            builder.Register<IHighScoreRepository,HighScoreRepositoryPlayerPrefs>(Lifetime.Singleton);
        }
        else
        {
            builder.Register<INameGetter,NameGetterConst>(Lifetime.Singleton);
            builder.Register<IHighScoreRepository, HighScoreRepositoryServer>(Lifetime.Singleton);
        }

        builder.Register<GameController>(Lifetime.Singleton);
        builder.RegisterComponent(gameControllerLoader);
    }
}

 やってることはZenject/Extenjectと同じです。唯一違うのがこいつです。

builder.RegisterComponent(gameControllerLoader);

 これはHierarchyにあるGameObjectについて、インジェクションをして欲しい対象を指定しなければいけません。Zenject/Extenjectはこれを自動でやってくれてたんですね。

インジェクションの種類

 [Inject]属性のあるフィールドやコンストラクタで必要なインスタンスを放り込んでくれることをそれぞれ、フィールドインジェクションやコンストラクタインジェクションと言います。メソッドインジェクションというのもあります。

[Inject]
public void Construct(GameController gameController)
{
    //コンストラクタ代わりに実行される
}

 Zenject/Extenject、VContainer共通です。
 原則的にはコンストラクタによるインジェクションを基本とします。そもそも何かに依存するのを避けるために、DIコンテナを使わない方法から出発しました。なのにDIコンテナがないと動かないというのはちょっと矛盾します。
 とはいえ、MonoBehaviourはコンストラクタを持てないので、代わりにフィールドインジェクションやメソッドインジェクションを使います。

DIコンテナは重い?

 まぁ重いとは言われます。リフレクションと言って、ソースコードの文字列を解析して、インジェクションが必要な所があれば、都度そこにインスタンスを放り込むという処理を行っているためです。そのためゲームを立ち上げるときなんかにちょっと時間がかかるようになるかもしれません。支障が出るほど重くなるのはちょっと考えにくいとは思います。

DIの意義

 おおよそのやり方は上述の通りです。これで何がしやすくなるのか。例えばテストがしやすくなります。サーバーが無くてもハイスコア関係の仮実装が可能で、それによってGameControllerも動かせるようになったのは見ての通りです。
 また複数人で作るのにも有効ではないでしょうか。HighScoreRepositoryServerができていないから、GameControllerのOnFinishedが完成しない、といった事態も避けられます。
 設計にお悩みの方は是非試してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor Tutorialまとめ

発端

Blazorを勉強するのになんかいい教材ないかなぁと探してたらYoutubeにチュートリアル動画(英語)が上がっていたので自動翻訳字幕見ながらまとめてみました。
https://www.youtube.com/watch?v=xr56fmgLl74&list=PL4WEkbdagHIR0RBe_P4bai64UDqZEbQap

★★★★注意★★★★
翻訳ではなく
「そんな感じのこと言ってるなぁ」
「全然違うこと言ってるけどつまりはそういうことか」
ということをまとめてます。

EP01:Blazorの基礎

  • ASP.NETでサーバーサイドで処理してたけど、サーバーのリソースいっぱい使うよね
  • react angular vueとかでクライアントサイドのリソース使えばサーバーのリソース使わなくて済むよね
  • でも認証コート書くわけにはいかないし、複数の言語使うの面倒だよね
  • WebAssembly使えばサーバーサイドもクライアントサイドも全部C#で書けるよ
  • VisualStudioでBlazorのプロジェクト作るとサンプルが勝手にできるよ
  • 動かしてみるとどんな感じでリクエスト飛んでるかよくわかるね。

EP02:サンプルの解説とページの追加

  • Index.razor」「Counter.razor」「FetchData.razor」って奴らがナビゲーションをクリックしたときに表示されるWebページだよ。
  • 各ファイルの頭に「@page "(パス)”」ってなってるね。例えば「Counter.razor」なら「@page "/counter"」とか。
  • Counter.razor」見てみるとHTMLと「@code{}」みたいなのが書かれてるね
  • Counter.razor」みたいに、HTMLの部分に「@変数名」「@onclick="関数名"」、「@code{}」の中に変数宣言と関数って書くと動くんだね。
  • Counter.razor」でいうところの「@currentCount」「@onclick="IncrementCount"」と「private int currentCount = 0;」「private void IncrementCount(){currentCount++;}」だね。
  • FetchData.razor」では天気と気温をテーブルに表示してるね。
  • FetchData.razor」のHTML部分ではforecastsっていうオブジェクトからデータを取ってきてforeachで回してるね。
  • forecastsはコード部分に宣言されててWeatherForecastクラスの配列だね。
  • WeatherForecastクラスは「WeatherForecast.cs」で宣言されてるね。
  • forecastsOnInitializedAsync関数をオーバーライドして初期化の時に非同期でForecastServiceっていうオブジェクトから引数に現在の時間を指定して値を設定してるね。
  • ForecastServiceWeatherForecastService.csで定義されてるね。

 (Task.FromResultってなんやろう?)
 タスク ベースの非同期プログラミング
 https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/task-based-asynchronous-programming
 C# 並行・並列プログラミング パターン集
 https://qiita.com/takutoy/items/c384fcb439d345a9a0d3
 →そうか、このメソッドが非同期処理で、戻り値がTask<WeatherForecast[]>やからこれを使うんやな。
 
+ (「Enumerable.Range(1, 5).Select」…?)
 [C#] 連番の作成 (Enumerable.Rangeの利用)
 https://qiita.com/koara-local/items/534376eabd11c64d31af
 →連番作成のメソッドとLINQの組み合わせかぁ
 
 (アロー演算子もよう知らんねんなぁ…)
 デリゲート - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
 https://ufcpp.net/study/csharp/sp_delegate.html
 →関数ポインタの考えを発展させたみたいな感じやな。
 
 古典的に書き換えるとこんな感じか。
 LINQもアロー演算子も確かにコード量は減るけど、初学者には厳しいなぁ…

var rng = new Random();
var ret = new WeatherForecast[5];
for (int i=0;i<5;i++)
{
var tmp = new WeatherForecast();
tmp.Date = startDate.AddDays(i);
tmp.TemperatureC = rng.Next(-20, 55);
tmp.Summary = Summaries[rng.Next(Summaries.Length)];
ret[i] = tmp;
}
return Task.FromResult(ret);
  • ForecastServiceが使えるのは「FetchData.razor」の上で「@inject WeatherForecastService ForecastService」って書いてるからだよ。
  • じゃ自分でページを追加してみようか

 (ちょっとまて、ForecastServiceの実体はどこやねん)
 →EP04で解説されてた。「Startup.cs」のConfigureServicesメソッド内
  「services.AddSingleton<WeatherForecastService>();
  って感じでシングルトンつくって@injectで使ってるんか。

  • Authors.razor」っていう名前でRazorコンポーネント追加するよ
  • Authors.razor」の頭に「@page "/authors"」って書くよ。「Index.razor」「Counter.razor」「FetchData.razor」と一緒だね。
  • MainLayout.razor」と「NavMenu.razor」を見てみよう。
  • NavMenu.razor」には「@page」が無いね。他のRazorコンポーネントから部品として呼ばれるようにするにはこんな風に書くよ。
  • MainLayout.razor」には「」って書いてあるね。こうやって「NavMenu.razor」を組み込んでるんだよ。
  • MainLayout.razor」の「@Body」は「NavMenu.razor」の操作で変わるよ。  (詳しく説明はないけど「MainLayout.razor」に「@inherits LayoutComponentBase」ってあるからLayoutComponentBaseって奴から継承した機能なんかなぁ)
  • NavMenu.razor」のメニューコピペ編集したらAuthorsってメニューが追加されてクリックしたら追加した「Authors.razor」が表示されたね。

(とりあえずEP10まで見たけど、まとめると結構時間かかる)

EP03:ルーティング(複数のルーティング/パラメーター/ナビゲーションマネージャー))

  • @page」を複数設定して別のURLでも同じページに遷移したり、「@page」でパラメーターを指定したりできるよ。
  • その前にBrazorがどこから開始してるか見てみよう
  • Program.cs」の中にMain関数があるね。ここからStartupへ飛ぶよ。

 (うあぁ、むっちゃラムダ式やん。詳しく見て言ってたらこれだけで1~2週間かかりそう)
 (とりあえず定義だけ追いかけとこうか)
 static IHostBuilder CreateDefaultBuilder(string[] args);
 static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure);
 (thisってなんやろう?C++のthisポインタとは違うと思うけど)
 →C# 3.0の拡張メソッドっていうんか。
 https://ufcpp.net/study/csharp/oo_static.html

static class Extensions
{
    public static int Parse(this string str)
    {
        return int.Parse(str);
    }
}

って書くと

int x = "1".Parse();

って書ける。便利やん。

  • Startup.cs」では一番下の方にあるendpoints.MapBlazorHub();でリクエストの受付準備してendpoints.MapFallbackToPage("/_Host");で「_Host.cshtml」にルーティングしてるね。
  • この「_Host.cshtml」でCSSを読み込んでるよ。
  • それからJavaScriptを読み込んでBlazorが動き出すよ。
  • 最初は「App.razor」が呼ばれるよ。
  • ここにはルーターが書かれていて、ここでブラウザから渡されたURLが有効なものかを判定しているよ。
  • 有効なURLなら<Found>の中身が、無効なURLなら<NotFound>の中身が返されるよ
  • @routeDataでパラメーターを渡してるのも確認できるね。
  • それじゃ試しに<NotFound>の中に<h1>404 Error</h1>って書いてデバッグ実行してみよう。
  • 試しに無効なURLを指定してみるとちゃんと404 Errorって表示されてるね。
  • それではEP02で作った「Authors.razor」を改造してルーティングについてもう少し詳しく見よう。
  • まずは「AuthorsDetail.razor」を追加して同ファイルの頭に@page "/authors/authordata"を追加しましょう。あと著者の詳細を書こう。
  • Authors.razor」には著者の名前書いて/authors/authordataのリンクを張ろう。
  • 動かしてみよう。「Authors.razor」の著者名をクリックしたら「AuthorsDetail.razor」へ飛ぶね。
  • 別のルーティングを追加してみよう。「AuthorsDetail.razor」の@page "/authors/authordata"の下に@page "/author"を追加してみよう。
  • 動かして直接/authorに飛んでみるとちゃんと「AuthorsDetail.razor」が表示されるね。
  • ここで同じルーティングがあるとどうなるか見てみよう。
  • さっき書いた「AuthorsDetail.razor」の@page "/author"@page "/authors"に変えて「Authors.razor」と同じにしてみよう。実行すると例外が出るのが確認できるね。
  • AuthorsDetail.razor」の@page "/authors"@page "/author"に直しておいて、次はパラメーターについてみてみよう。
  • AuthorsDetail.razor」の@page "/authors/authordata"@page "/authors/authordata/{authorId}"に変えよう。
  • 受け取ったパラメーターを使うために@codeに以下を追加しよう。
[Parameter]
public string AuthorId { get; set; };
  • HTML部の著者の名前の前に@AuthorIdをつけよう。  (パラメータ名は大文字/小文字の区別ないみたいやね。URLやし当然か。けど混乱しそうやから同じにするようにしたほうが良さそうやね)
  • とりあえずこの状態で実行して「Authors.razor」から「AuthorsDetail.razor」に飛んでみるとエラーになるよね。
  • つまり@page "/authors/authordata/{authorId}"はパラメータが必須になるということだね。
  • パラメータなしも許容したい場合は@page "/authors/authordata/{authorId}"だけでなく@page "/authors/authordata"も書いてあげればいいよ。
  • こうやって書くとパラメータなしでも動いて、パラメータを手で入れてやるとちゃんと表示されるのがわかるね。
  • public string AuthorIdpublic int AuthorIdって宣言して@page "/authors/authordata/{authorId:int}"って書いてやるとパラメーターの型指定ができるよ。詳しくはASP.NET Core Blazor のルーティングを見てね。

 (bool,datetime,decimal,double,float,guid,int,long の8つが使えるみたいやね。)
+ では次に「ボタンを押してページ遷移」を実装してみよう。
+ とりあえず「Counter.razor」から<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>private void IncrementCount()を「Authors.razor」へコピーして関数名をNavigateに変えよう。
+ 「Authors.razor」の頭に@inject NavigationManager NavigationManagerを追加しよう。
+ Navigate関数の処理にNavigationManager.NavigateTo("/authors/authordata/22");を追加しよう。
+ あとリンクの方も"/authors/authordata/11"に変えておこう。
+ 動かしてみよう。思った通りに動くね。
+ これ以外にもいろいろやり方があるよ。
+ 「App.razor」がルーティングの起点になってるって覚えておいてね。

EP04:依存性注入

  • 今回は「依存性注入(Dependency injection)」について話すよ!難しくないよ!

 (依存性注入の概念はwikipediaとか見たほうが良さそう)
 依存性の注入 - wikipedia)

EP05:Forms & Validations

EP06:JavaScript Interop | Calling JavaScript from C

EP07:Razor Components | Re-usability

EP08:Razor Components | Lifecycle Methods

EP09:Razor Component Libraries

EP10:Call REST API | CRUD Methods

EP11:Authentication | Out of the box

EP12:Authentication | Custom AuthenticationStateProvider

EP13:Layouts | Login Pages

EP14:HttpClient | Login User

EP15:IHttpClientFactory | Login User

EP16:Sending JWT token & Building Request Middleware

EP17:Register User & Generate JWT

EP18:Role-based Authorization

EP19:Policy-based Authorization

EP20:Procedural Logic | Authentication & Authorization in C

EP21:Templated Components | Html Table

EP22:Razor Components | EventCallback

EP23:Event Handling

EP24:GridView Header Filter

EP25:Gridview Paging

EP26:Spinner or Activity Indicator : EP26

EP27:Code Faster Using dotnet watch run

EP28:Deploy to IIS

EP29:Deploy to Azure App Services

EP30:Handling Exceptions

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【.NET】Oracle Database + ODP.NET Core Managed Driver実装方法のまとめ

.NET CoreからOracle Databaseに接続するには、ODP.NET Core Managed Driverを使います。さらに、これはOracle Clientのインストールなしで使用できます。

今回の記事ではC#のプログラムから、ODP.NET Core Managed Driverを使って、Oracle Database 19cに接続する方法、および詳細をまとめてみたいと思います。

ちなみに、今回の開発環境は以下の通り。

  • .NET 5.0 (C#)
  • Visual Studio 2019 for Mac
  • macOS Mojave
  • Oracle Database 19c(19.3)
  • ODP.NET Core Managed Driver

下で書きましたが、ODP.NET Core Managed Driverは正式には.NET5.0 未サポートです(.NET Core 2.xまでは正式サポート)。ただ、コーディングの方法は恐らく変わらないのではないかなと思うのと(変わってたら、すみません)、トップレベルステートメントが書きやすいので.NET 5.0で例を実装しています。

あと、Oracle Databaseを単にOracleと言うことにします(Oracleは社名で製品名ではない)。

.NET 5.0の正式対応はまだのよう

下は「開発者ガイドfor Microsoft Windows」のシステム要件からの引用ですが、最新の19cに関しても、.NET Core 3.xは未サポートのようです(当然5.0も同様でしょう)。

ODP.NET Coreには、次のものが必要です。

・オペレーティング・システム:
 ・管理対象および管理対象外ODP.NETと同じWindowsオペレーティング・システムのサポート
 ・Oracle Linux 7
 ・Red Hat Enterprise Linuxの場合 7
・Microsoft .NET Framework
 ・Microsoft .NET Core 2.1以降
 ・Microsoft .NET Framework 4.6.1以降
   :writing_hand: 注意:.NET Core 3.xは現在サポートされていません

また、補足すると、従来からのUnmanaged Driver(Oracleは管理対象外と表現)を使用する場合は、以下の.NET Frameworkが対象で、Oracle Clientも必要です。

ODP.NET for .NET Framework 2.0は、Microsoft .NET Framework 3.5 SP 1以降でのみサポートされています。
ODP.NET for .NET Framework 4は、Microsoft .NET Framework 4.5.2、4.6.xおよび4.7.xでのみサポートされます。

Coreが付かないODP.NET Managed Driverも.NET Frameworkが対象のものですが、こちらはOracle Clientは必要ありません。

詳細は、ODP.NETのシステム要件をご覧下さい。

まぁ、でも、せっかく.NET 5.0が手元にあるので、今回は5.0で試してみようと思います。

(補足)Oracle公式とMicrosoftで日本語訳が違う

Oracleマニュアルの開発者ガイドfor Microsoft Windowsを見てもらえば分かりますが、Oracle Data Provider for .NET管理対象ドライバとか管理対象外ドライバという言い方がされています。

恐らく訳者の方がご存じなかったのだと推測しますが、Microosftの.NETのサイトだと、マネージドとかアンマネージドと表現されているものが、これにあたります。英語の"managed"で、CLRで管理されるので「管理対象」という訳語は意味は間違ってませんが、マイクロソフトの日本語サイトでも"マネージドコード"という表現がされているので合わせて欲しい気はしますが、とにかく日本語訳が違っています(Google検索で引っかかりにくくなるしね…)。

Wikipediaで恐縮ですが、Managed Codeのページにも、"The term was coined by Microsoft.(この用語はMicrosoftにより造語された)"とあります。一般的な名称というよりは、Microsoftだけで使う用語のようです。

ちなみに、Javaだと同じ位置づけのものは「バイトコード(bytecode)」ですかね?

ちなみに、この記事では、特に必要が無い限りは「Managed」とかマネージドという表現をします。

環境の準備

ODP.NET Core Managed Driver追加

NuGetから、Oracle.ManagedDataAccess.Coreを探し、「パッケージの追加」でインストールします。

スクリーンショット 2020-11-22 13.44.00.png

接続先情報の追加

ODP.NET Core Managed Driverは、.NETの構成ファイル(app.config、web.config、machine.config)をサポートしていません。補足ですが、.NET Framework用のODP.NET Managed Driverでは構成ファイルをサポートしています。

可能な方法は以下の3つ。

  • コード中で接続文字列を指定
  • .NET構成API(OracleConfiguration)を使用
  • tnsnames.oraを使用

以下の実装例で実際のコードを書いてみます。

ODP.NET Core Managed Driver 実装例

普通は抽象化してDAO (Data Access Object)を作成するかもしれませんが、今回は直書きします。あと、ネストが深くなると見にくいと思ったので、usingを使用していません。

まずは、接続の方法から。

接続方法1:コード中で接続文字列を直接記述

tnsnames.oraがなくても接続できます。まぁ、以前からある方法だと思います。

using System;
using Oracle.ManagedDataAccess.Client;

var connectionString = "user id=scott;password=tiger;data source=192.168.1.101/pdb1";
OracleConnection pdb1Con = null;

try
{
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();
    Console.WriteLine("Success");
}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();
}

接続方法2:.NET構成API(OracleConfiguration)

静的クラスのOracleConfigurationを使用します。

using System;
using Oracle.ManagedDataAccess.Client;

//接続識別子
OracleConfiguration.OracleDataSources.Add("pdb1", "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.1.101)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=pdb1)(SERVER=dedicated)))");

var connectionString = "user id=scott;password=tiger;data source=pdb1";
OracleConnection pdb1Con = null;

try
{
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();
    Console.WriteLine("Success");

}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();
}

また、OracleConfigurationで詳細な設定が可能です。詳細は、以下。

接続方法3:tnsnames.ora

tnsnames.oraの検索の優先順位は以下。

  • OracleConfiguration.TnsAdminプロパティで設定されたディレクトリ
  • 実行中ODP.NET Coreアセンブリのディレクトリ
  • 現行作業ディレクトリ

実行時フォルダに以下のtnsnames.oraがあるとします。
スクリーンショット 2020-11-22 17.26.14.png

tnsnames.ora
PDB1 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.101)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = pdb1)
    )
  )

コードはほぼ同じで、接続情報の部分がないだけです。

using System;
using Oracle.ManagedDataAccess.Client;

var connectionString = "user id=scott;password=tiger;data source=pdb1";
OracleConnection pdb1Con = null;

try
{
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();
    Console.WriteLine("Success");

}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();
}

上でも説明しましたが、OracleConfiguration.TnsAdminでtnsnames.oraがあるディレクトリを指定する方法でも可能です。

using System;
using Oracle.ManagedDataAccess.Client;

//tnsnames.oraがあるディレクトリを指定
OracleConfiguration.TnsAdmin = "/tmp";

var connectionString = "user id=scott;password=tiger;data source=pdb1";
OracleConnection pdb1Con = null;

try
{
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();
    Console.WriteLine("Success");

}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();
}

データ取得

基本は以前のODP.NETと変わらないはずです。
(コードが汚かったら、教えて下さい)

using System;
using Oracle.ManagedDataAccess.Client;

//tnsnames.oraのディレクトリを指定
OracleConfiguration.TnsAdmin = "/tmp";

var connectionString = "user id=scott;password=tiger;data source=pdb1";
OracleConnection pdb1Con = null;
OracleCommand pdb1Cmd = null;
OracleDataReader rdr = null;

try
{
    //接続
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();

    if (pdb1Con.State == System.Data.ConnectionState.Open)
        Console.WriteLine("Successfully Connected.");

    //DBの日時を取得
    pdb1Cmd = pdb1Con.CreateCommand();
    pdb1Cmd.CommandText = "select to_char(sysdate,'YYYY/MM/DD HH24:MI:SS') from dual";
    rdr = pdb1Cmd.ExecuteReader();

    //出力
    while (rdr.Read())
        Console.WriteLine("Time: " + rdr.GetString(0));
}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();

    if (pdb1Cmd != null)
        pdb1Cmd.Dispose();

    if (rdr != null)
        rdr.Dispose();
}

更新

これも、従来から同じですかね。ExecuteNonQueryで実行します。

using System;
using Oracle.ManagedDataAccess.Client;

//tnsnames.oraのディレクトリを指定
OracleConfiguration.TnsAdmin = "/tmp";

var connectionString = "user id=scott;password=tiger;data source=pdb1";
OracleConnection pdb1Con = null;
OracleCommand pdb1Cmd = null;
OracleDataReader rdr = null;

try
{
    //接続
    pdb1Con = new OracleConnection(connectionString);
    pdb1Con.Open();

    if (pdb1Con.State == System.Data.ConnectionState.Open)
        Console.WriteLine("Successfully Connected.");

    //データ追加
    pdb1Cmd = pdb1Con.CreateCommand();
    pdb1Cmd.CommandText = "insert into staff (id, name) values (100, 'Scott')";
    var rtn = pdb1Cmd.ExecuteNonQuery();

    //取得
    pdb1Cmd.CommandText = "select id, name from staff where name = 'Scott'";
    rdr = pdb1Cmd.ExecuteReader();

    //出力
    while (rdr.Read())
        Console.WriteLine($"id: {rdr.GetInt16(0)}, name:{rdr.GetString(1)}");

}
catch (Exception ex)
{
    Console.WriteLine($"Connection Failed: {ex.Message}");
}
finally
{
    if (pdb1Con != null)
        pdb1Con.Close();

    if (pdb1Cmd != null)
        pdb1Cmd.Dispose();

    if (rdr != null)
        rdr.Dispose();
}

 
PL/SQLのPackageからデータを取得する方法についても、おいおい、ここに追記していきたいと思います。

個人的にポイントだと思ったところ

旧来のアンマネージドなODP.NETに比べ、DLLの数もファイルサイズも少ないのですっきりはしますし、クライアントをインストールしなくていいので、開発も容易になりそうです。

あと、クラスやメソッドなどAPIがまったく同じなので、混乱もしないでしょう(名前空間がOracle.ManagedDataAccessという違うくらい)。

ただ、ODP.NET Managed DriverとODP.NET Core Managed Driverでは利用可能なクラスが異なるようなので、移行する時は検証が必要となりそうです(対応表は以下のリンク参照)。

以上です。
まだ色々ありそうなので、この記事に追記していければと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

契約書をちゃんと()作ったC#湯婆婆

はじめに

この記事は、N高等学校アドベントカレンダー14日目の記事です。ちなみに僕はよわよわN高生です。C#の理解もめちゃくちゃ浅いです。

@NemesisさんのJavaで湯婆婆を実装してみるが若干ゃ流行っているのでC#で適当に書いてみました。
C#湯婆婆はとっくに実装している方がいらっしゃるので、契約書クラスをちゃんと()作ったものを実装してみました。ついでにほんのちょっと機能が改善されています、たぶん、おそらく、きっと。(ぶっちゃけコードはほとんど変わりませんが、せっかく作ったので)

一応ネタバレになるかもしれないのでそこは注意してください。

コード

Yubaba.cs
using System;
using System.Text.RegularExpressions;
using static System.Console;

namespace Yubaba
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("契約書だよ。そこに名前を書きな。");
            Contract contract = new Contract();
            contract.Name = ReadLine();
            string name = Regex.Replace(contract.Name, @"\s", "");
            WriteLine($"フン。{name}というのかい。贅沢な名だねぇ。");
            string newName = CreateNewName(name);
            WriteLine($"今からお前の名前は{newName}だ。いいかい、{newName}だよ。分かったら返事をするんだ、{newName}!!");
            ReadLine();
        }

        static string CreateNewName(string name)
        {
            if (name == "荻野千尋") return "千";
            Random random = new Random();
            return name[random.Next(name.Length)].ToString();
        }
    }

    public class Contract
    {
        public string Text { get; set; } = "契約書の本文が適当に書いてある";
        public string Name { get; set; } //署名
    }

}

まあ契約書(Contract)クラスを作ったり、名前を奪う部分をメソッドにしたりと(そこまで必要のないことを)したぐらいで、特に変わった実装はないです。(ただ契約書を作成して署名させるという流れを忠実に再現しようとしたらこういう実装になりました)

ただ、「山田 太郎」のように空白なんかを入れると空白が名前になってしまう可能性が出てくるので、

string name = Regex.Replace(contract.Name, @"\s", "");

の部分で正規表現を使って空白を消しています。(こうするとTabなんかも消せることをgoogle先生に教わりました)
また、「荻野千尋」と入力した場合は確定で「千」になるようにしています。(原作の原作の再現)

実行例

契約書だよ。そこに名前を書きな。
山田太郎
フン。山田太郎というのかい。贅沢な名だねぇ。
今からお前の名前は山だ。いいかい、山だよ。分かったら返事をするんだ、山!!
契約書だよ。そこに名前を書きな。
        タロ =  ウ      = ヤマダ
フン。タロ=ウ=ヤマダというのかい。贅沢な名だねぇ。
今からお前の名前はヤだ。いいかい、ヤだよ。分かったら返事をするんだ、ヤ!!
契約書だよ。そこに名前を書きな。
荻野 千尋
フン。荻野千尋というのかい。贅沢な名だねぇ。
今からお前の名前は千だ。いいかい、千だよ。分かったら返事をするんだ、千!!

名前が空の場合はお察し

おわりに

当然といえば当然なんですが、千尋の名前を奪うところはもっといろいろセリフがあったんですね、見たのが昔すぎてほとんど覚えていない…

若干ふざけたコードなのであれですが、変なところがあれば教えてもらえるとありがたいです。

別の方で参考にさせてもらった記事

このコードで対応していない?田さんを働かせる方法
C# で湯婆婆を実装してみる(?田さんにも対応)

早い湯婆婆
C# で湯婆婆を実装してみる「性能のいい湯婆婆を目指して」
C#で性能のいい湯婆婆

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】ランダムで沢山の武器の中から三つを選べるシステムを実装してみた

この記事は?

先日stgゲームを作っている際に、タイトル通りのことを作ってみようと思ったのですが、
自分にとって難しかったので実装までの道のりを備忘録的に書いてみました。

実装までの道のり

まず、問題の切り分けをしました。ランダムで沢山の武器の中から三つを選ぶシステムを作るためには、
1 沢山の武器を作り、配列の中に入れる。
2 乱数を使って三つの「被らない」乱数を添え字として使う。
3 プレイヤーが選んだことを検知して選んだ武器を渡す。
この3つがあれば実装できそうなので、順に実装していきました。

1 沢山の武器を作り、配列の中に入れる。

あとから武器を追加する可能性があるので、静的配列ではなく動的配列を作り、順次武器を作ってその中に入れていきます。

2 乱数を使って三つの「被らない」乱数を添え字として使う。

これが正直一番大変でした。ただwhileを使って被らないような内容を作るだけなのですが、
for文やらif文やらが混雑してなんの文がなにをしているかがわからなくなってしまい、
解読に時間がかかったり、Unityがフリーズしたりと。
切り分けて一つずつ問題を解決していけば難しくないことに気が付きました。

3 プレイヤーが選んだことを検知して選んだ武器を渡す。

これはゲームを作ったことのある人なら誰もが覚えのあるOnButtonClickメソッドで、
選んだ武器の情報を取得すればよさそうです。

最後に

Listとかを使うときはAddやらRemoveやらを使わなければならないみたいなので使うのを尻込みしていましたが、
色々検索してみたら意外にもなんとかなったのでGoogle先生に感謝しながらコード書いていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++で作成したdllをSwigで作ったWrapper経由でC#から呼び出す方法

はじめに

みなさん、OpenVINO使っていますか?
OpenVINOはIntel製CPUやGPU、VPU等の上で高速に推論をするエンジンなのですが、C#のインターフェースは提供されていません。
C/C++かPythonで書く必要がありますが、私は C# を使いたいのです!
ということで、SwigでWrapperを作ろうと決意したわけですが、作ったことがないので調べてみました。

私と同じように思っている方がどの程度いるか不明ですが、その方の助けの一部になればと思います。

この記事は以下5つのパートに分かれます。
1. C++でdll作成
2. C++からdll呼び出し
3. Wrapper作成
4. C#からWrapper経由でのdll呼び出し
5. 結論

お先に結論

SwigでWrapperを作りたいのですが、現時点では失敗しています。
Wrapper経由でC#から呼び出そうとすると例外を吐きます。
詳細は結論に記載していますが、詳しい方からアドバイスいただけるかもしれないと思い、この状態で投稿しております。
もし原因わかる方、これじゃないの?と思う方、コメントいただけると幸いです。

[2020.11.23追記]
原因が判明しましたので記事とソースを修正しました!

環境

今回dllやWrapperを作成した環境は以下です。
- Windows10 Pro(x64)
- Visual Studio 2019 Professional
- Swig 4.0.2

ソースコード

ソースコードはここに格納しています。
プロジェクトとしては以下3つになります。
- HellDll
  C/C++で作成するdllのプロジェクトです。
- HellpCpp
  作成したdllをC++から呼び出すことが出来るのかテスト用のプロジェクトです。
- HelloCSharp
  作成したdllをC#から呼び出すことが出来るのかテスト用のプロジェクトです。

C++でdll作成

今回はClassifierという名前のクラスとそのクラスを生成する関数をdllで提供しますので、そのためのプロジェクトをHelloDllという名前で作成していきます。

作成したヘッダ(Classifier.h)の中身は以下です。

Classifier.h
#pragma once

#ifdef DLL_EXPORT
#define DLL __declspec(dllexport)
#else
#define DLL __declspec(dllimport)
#endif

class Classifier
{
public:
    virtual bool Train(int val) = 0;
    virtual bool Fit() = 0;
};

extern "C" DLL Classifier * GetInstance(void);

dllを作成する部分の詳細はC++で作成したDLLでAPIではなくクラスを提供する方法のやり方を踏襲していますので、そちらを参照してください。

C++からdll呼び出し

C++で作成するコンソールプログラムから作成したdllが呼び出せるか確認します。
プロジェクト名はHelloCppです。
呼び出す部分は以下となっています。

HelloCpp.cpp
HMODULE hHandle = LoadLibrary(L"HelloDll.dll");
FUNC func = (FUNC)GetProcAddress(hHandle, "GetInstance");
Classifier* instance = func();
instance->Train(10);

こちらも、C/C++で作成したdllをC++から呼び出す部分もC++で作成したDLLでAPIではなくクラスを提供する方法のやり方を踏襲していますので、そちらを参照してください。

Wrapper作成

Wrapperを作成する方法はいくつかあるようなのですが、すべて手作業でするのも面倒なのでSwigを使ってみました!

SwigでWrapperを作るのは簡単で、インターフェースファイルと呼ばれる.iファイルを作成するだけです。ただ、これが奥深く、理解するのが大変というか、まだ私自身理解できていないという・・・

とりあえず、Swigを使ってWrapperを作っていきましょう。

Swigのインストール

ここからサクッと落としましょう
2020年11月21日にこの記事を記載していますが、現時点での最新は4.0.2となっています。
私の環境はwindowsなのでswigwin-4.0.2をダウンロードすることになります。
お使いの環境に合わせてダウンロードしてください。

ダウンロード後は、適当な場所に展開しパスを通すなりしてください。
これでインストールは完了です。

インターフェースファイルの作成

ようやくSwigの肝であるインターフェースファイルの作成です。
マニュアルはここにありますので、まずは熟読しましょう。

で、とりあえず必要最低限の内容をカバーしたインターフェースファイルは以下のようになります。

/* File : Swig.i */

/* windows.iをincludeしないと、なぜかエラー */
%include <windows.i> 

%module HelloDll

%{
#include "Classifier.h"
%}

/* Let's just grab the original header file here */
%include "Classifier.h"

ポイントは以下です。
- %moduleの後ろにdll名
- %{}の中に提供するヘッダ名を記載
- %includeの後ろに提供するヘッダ名を記載
- エラー対策にwindows.iを最初にinclude

最後のwindows.iをincludeする部分ですが、広大なネットの海を探しているときにそのような情報を見つけたのですが、今となってはその記述を見つけられず。。。
とりあえず効果があるので記載していますが、どのような理由でエラーが回避できるのかなどは不明です。

インターフェースファイルのビルド

コマンドプロンプトで毎回ビルドしても良いのですが、Visual Studioでdllをビルドした際に一緒にビルドするようにしましょう。
手順は以下です。
1. 作成したインターフェースファイル(今回はSwig.i)をdllのプロジェクト(今回はHelloDll)に加える
2. 1.で加えたSwig.iをVisual Studio上で右クリックしプロパティを開く
3. 項目の種類をカスタムビルドツールに変更
4. 3.を実行すると左側にカスタムビルドツールが表示されるようになるので、コマンドラインに以下を記載

"C:\Program Files\swigwin-4.0.2\swig.exe" -c++ -csharp  -cppext cpp "%(FullPath)"

5.出力ファイルに以下を記載

%(Filename)_wrap.cpp; %(Outouts)

1.~5.が完了後、HelloDllプロジェクトをリビルドして、以下のファイルが作成されれば成功です。
- Classifier.cs
- HelloDll.cs
- HelloDllPINVOKE.cs
- Swig_wrap.cpp

[2020.11.23 追記]
Swig.exeが作成した上記4つのファイルのうち、Swig_wrap.cppをHelloDllプロジェクトのソースファイルフォルダに追加します。これが必要でした!

C#からWrapper経由でのdll呼び出し

C#で作成するコンソールプログラムから作成したdllが呼び出せるか確認します。
プロジェクト名はHelloCSharpです。

することは2つだけです。
1. Swig.exeが作成した.csファイルをプロジェクトに加える
2. dllが提供する関数を呼び出す

1.は特にわからない部分はないと思いますので割愛します。
2.ですが、呼び出す部分は以下となります。

Program.cs
Classifier  classifier= HelloDll.GetInstance();
classifier.Train(10);

では実際に動作するか見てみましょう!




はい、動きませんね。
私にもわかりません(汗)
dll自体はC++側で呼び出せているので問題ないはずなので、やはりSwigの使い方だと思うのですが、謎です。。。

[2020.11.23 追記]
問題なければ以下が表示されるはずです。

Hello World!
GetInstance()
ClassifierImpl
Train : val = 10

結論

Swigを使ってWrapperを作り、C++のdllをC#から呼び出したかったのですが、C#側で呼び出した途端、以下の例外を吐いて失敗します。

TypeInitializationException: The type initializer for 'SWIGExceptionHelper' threw an exception.

EntryPointNotFoundException: Unable to find an entry point named 'SWIGRegisterExceptionCallbacks_HelloDll' in DLL 'HelloDll'.

解決策を探している途中なのですが、にっちもさっちもいかないので、アドバイスいただけたらと思いQiitaに投稿しました。
もし原因わかる方、これじゃないの?と思う方、コメントいただけると幸いです。
何かしら情報にアップデートあれば本記事を更新いたします。

[2020.11.23 追記]
無事、C++で作成したdllをSwigでWrapperを作成してC#から呼び出すことが出来ました。
Wrapper開発の工数削減を目的にSwigを使いましたが、インターフェースファイル(.iファイル)を用意するだけなので、インターフェースファイルさえ問題なく作ることが出来れば目的は達せられるという印象を受けました。
今回は簡単にするために引数や戻り値を簡単なものにしましたが、どうやら文字列や構造体、クラスやポインタなど色々難しそうですが、少しずつ調べていきたいと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++で作成したdllをWrapper経由でC#から呼び出す方法

はじめに

みなさん、OpenVINO使っていますか?
OpenVINOはIntel製CPUやGPU、VPU等の上で高速に推論をするエンジンなのですが、C#のインターフェースは提供されていません。
C/C++かPythonで書く必要がありますが、私は C# を使いたいのです!
ということで、SwigでWrapperを作ろうと決意したわけですが、作ったことがないので調べてみました。

私と同じように思っている方がどの程度いるか不明ですが、その方の助けの一部になればと思います。

この記事は以下5つのパートに分かれます。
1. C++でdll作成
2. C++からdll呼び出し
3. Wrapper作成
4. C#からWrapper経由でのdll呼び出し
5. 結論

お先に結論

SwigでWrapperを作りたいのですが、現時点では失敗しています。
Wrapper経由でC#から呼び出そうとすると例外を吐きます。
詳細は結論に記載していますが、詳しい方からアドバイスいただけるかもしれないと思い、この状態で投稿しております。
もし原因わかる方、これじゃないの?と思う方、コメントいただけると幸いです。

[2020.11.23追記]
原因が判明しましたので記事とソースを修正しました!

環境

今回dllやWrapperを作成した環境は以下です。
- Windows10 Pro(x64)
- Visual Studio 2019 Professional
- Swig 4.0.2

ソースコード

ソースコードはここに格納しています。
プロジェクトとしては以下3つになります。
- HellDll
  C/C++で作成するdllのプロジェクトです。
- HellpCpp
  作成したdllをC++から呼び出すことが出来るのかテスト用のプロジェクトです。
- HelloCSharp
  作成したdllをC#から呼び出すことが出来るのかテスト用のプロジェクトです。

C++でdll作成

今回はClassifierという名前のクラスとそのクラスを生成する関数をdllで提供しますので、そのためのプロジェクトをHelloDllという名前で作成していきます。

作成したヘッダ(Classifier.h)の中身は以下です。

Classifier.h
#pragma once

#ifdef DLL_EXPORT
#define DLL __declspec(dllexport)
#else
#define DLL __declspec(dllimport)
#endif

class Classifier
{
public:
    virtual bool Train(int val) = 0;
    virtual bool Fit() = 0;
};

extern "C" DLL Classifier * GetInstance(void);

dllを作成する部分の詳細はC++で作成したDLLでAPIではなくクラスを提供する方法のやり方を踏襲していますので、そちらを参照してください。

C++からdll呼び出し

C++で作成するコンソールプログラムから作成したdllが呼び出せるか確認します。
プロジェクト名はHelloCppです。
呼び出す部分は以下となっています。

HelloCpp.cpp
HMODULE hHandle = LoadLibrary(L"HelloDll.dll");
FUNC func = (FUNC)GetProcAddress(hHandle, "GetInstance");
Classifier* instance = func();
instance->Train(10);

こちらも、C/C++で作成したdllをC++から呼び出す部分もC++で作成したDLLでAPIではなくクラスを提供する方法のやり方を踏襲していますので、そちらを参照してください。

Wrapper作成

Wrapperを作成する方法はいくつかあるようなのですが、すべて手作業でするのも面倒なのでSwigを使ってみました!

SwigでWrapperを作るのは簡単で、インターフェースファイルと呼ばれる.iファイルを作成するだけです。ただ、これが奥深く、理解するのが大変というか、まだ私自身理解できていないという・・・

とりあえず、Swigを使ってWrapperを作っていきましょう。

Swigのインストール

ここからサクッと落としましょう
2020年11月21日にこの記事を記載していますが、現時点での最新は4.0.2となっています。
私の環境はwindowsなのでswigwin-4.0.2をダウンロードすることになります。
お使いの環境に合わせてダウンロードしてください。

ダウンロード後は、適当な場所に展開しパスを通すなりしてください。
これでインストールは完了です。

インターフェースファイルの作成

ようやくSwigの肝であるインターフェースファイルの作成です。
マニュアルはここにありますので、まずは熟読しましょう。

で、とりあえず必要最低限の内容をカバーしたインターフェースファイルは以下のようになります。

/* File : Swig.i */

/* windows.iをincludeしないと、なぜかエラー */
%include <windows.i> 

%module HelloDll

%{
#include "Classifier.h"
%}

/* Let's just grab the original header file here */
%include "Classifier.h"

ポイントは以下です。
- %moduleの後ろにdll名
- %{}の中に提供するヘッダ名を記載
- %includeの後ろに提供するヘッダ名を記載
- エラー対策にwindows.iを最初にinclude

最後のwindows.iをincludeする部分ですが、広大なネットの海を探しているときにそのような情報を見つけたのですが、今となってはその記述を見つけられず。。。
とりあえず効果があるので記載していますが、どのような理由でエラーが回避できるのかなどは不明です。

インターフェースファイルのビルド

コマンドプロンプトで毎回ビルドしても良いのですが、Visual Studioでdllをビルドした際に一緒にビルドするようにしましょう。
手順は以下です。
1. 作成したインターフェースファイル(今回はSwig.i)をdllのプロジェクト(今回はHelloDll)に加える
2. 1.で加えたSwig.iをVisual Studio上で右クリックしプロパティを開く
3. 項目の種類をカスタムビルドツールに変更
4. 3.を実行すると左側にカスタムビルドツールが表示されるようになるので、コマンドラインに以下を記載

"C:\Program Files\swigwin-4.0.2\swig.exe" -c++ -csharp  -cppext cpp "%(FullPath)"

5.出力ファイルに以下を記載

%(Filename)_wrap.cpp; %(Outouts)

1.~5.が完了後、HelloDllプロジェクトをリビルドして、以下のファイルが作成されれば成功です。
- Classifier.cs
- HelloDll.cs
- HelloDllPINVOKE.cs
- Swig_wrap.cpp

[2020.11.23 追記]
Swig.exeが作成した上記4つのファイルのうち、Swig_wrap.cppをHelloDllプロジェクトのソースファイルフォルダに追加します。これが必要でした!

C#からWrapper経由でのdll呼び出し

C#で作成するコンソールプログラムから作成したdllが呼び出せるか確認します。
プロジェクト名はHelloCSharpです。

することは2つだけです。
1. Swig.exeが作成した.csファイルをプロジェクトに加える
2. dllが提供する関数を呼び出す

1.は特にわからない部分はないと思いますので割愛します。
2.ですが、呼び出す部分は以下となります。

Program.cs
Classifier  classifier= HelloDll.GetInstance();
classifier.Train(10);

では実際に動作するか見てみましょう!




はい、動きませんね。
私にもわかりません(汗)
dll自体はC++側で呼び出せているので問題ないはずなので、やはりSwigの使い方だと思うのですが、謎です。。。

[2020.11.23 追記]
問題なければ以下が表示されるはずです。

Hello World!
GetInstance()
ClassifierImpl
Train : val = 10

結論

Swigを使ってWrapperを作り、C++のdllをC#から呼び出したかったのですが、C#側で呼び出した途端、以下の例外を吐いて失敗します。

TypeInitializationException: The type initializer for 'SWIGExceptionHelper' threw an exception.

EntryPointNotFoundException: Unable to find an entry point named 'SWIGRegisterExceptionCallbacks_HelloDll' in DLL 'HelloDll'.

解決策を探している途中なのですが、にっちもさっちもいかないので、アドバイスいただけたらと思いQiitaに投稿しました。
もし原因わかる方、これじゃないの?と思う方、コメントいただけると幸いです。
何かしら情報にアップデートあれば本記事を更新いたします。

[2020.11.23 追記]
無事、C++で作成したdllをSwigでWrapperを作成してC#から呼び出すことが出来ました。
Wrapper開発の工数削減を目的にSwigを使いましたが、インターフェースファイル(.iファイル)を用意するだけなので、インターフェースファイルさえ問題なく作ることが出来れば目的は達せられるという印象を受けました。
今回は簡単にするために引数や戻り値を簡単なものにしましたが、どうやら文字列や構造体、クラスやポインタなど色々難しそうですが、少しずつ調べていきたいと思います。

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Microsoft.ACE.OLEDBについてまとめてみた

はじめに

最近は、Windows 10 PCを購入すると64bitになっています。プリインストール版のOfficeも64bitです。
Windows 10 のバージョン2004からは、OEM用にて32bitが提供されなくなります。
https://docs.microsoft.com/en-us/windows-hardware/design/minimum/minimum-hardware-requirements-overview#31-processor

企業では使用アプリケーションやExcelのマクロやVBAなどの動作も考慮して、Windows 10 64bit版でもOffice 32bit版をインスールされていることが多いです。
下記サイトでは、Microsoftの推奨値を記載しています。
Microsoft Officeにて32bit、64bitどちらを使うか問題
Office 365・2019では64bitが推奨値、Office 2016、2010、2013では32bitが推奨値です。

この記事は、Visual Basic Advent Calendar 2020の先行記事となります。Visual Basicでは記事が少ないので、今年書いた記事ならOKにしています。

Microsoft.ACE.OLEDB

64bit版Windowsには「Microsoft.Jet.OLEDB.4.0」が提供されていません。MDBやExcelの操作をしようとした場合に「'Microsoft.Jet.OLEDB.4.0' プロバイダはローカルのコンピュータに登録されていません。」のエラーが発生します。
64bit版Windowsでの「Microsoft.Jet.OLEDB.4.0」について

その場合、「Microsoft.ACE.OLEDB.XX.0」に切り替えるなどの対応が必要になります。「Microsoft.ACE.OLEDB」は64bitと32bitの両方が提供されていますし、下記表のようにサポートするファイルが多くなります。

Engine dBase Access Text Excel
Jet .dbf .mdb .txt
.csv
.xls
Ace .dbf
.ndx
.mdx
.mdb
.accdb
.txt
.csv
.xls
.xlsx
.xlsm
.xlsb

OLEDBプロバイダの一覧取得

.NETにはOLEDBの一覧を取得するライブラリがあるので、これをPowerShellから呼べば一覧が取得できる。

64bit版
powershell.exe
PS > (New-Object data.oledb.oledbenumerator).getElements() | select SOURCES_NAME, SOURCES_DESCRIPTION
32bit版
%SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
PS > (New-Object data.oledb.oledbenumerator).getElements() | select SOURCES_NAME, SOURCES_DESCRIPTION

接続文字列

ACEプロバイダとAccesssバージョン

  • Microsoft.ACE.OLEDB.12.0(Access2007,Access2010)
  • Microsoft.ACE.OLEDB.15.0(Access2013)
  • Microsoft.ACE.OLEDB.16.0(Access2016)

※Access2013は Microsoft.ACE.OLEDB.12.0 として呼び出すと例外吐くようなので、15.0として呼び出すか、上のコンポーネントを入れます。2016は12.0 or 16.0どちらでも。
参照:ADO.NETでExcelファイルに接続する

マイクロソフトでは13という数字は忌み番のため内部バージョンはスキップされる。
13と色にまつわる話

ACEのインストール

「Microsoft.ACE.OLEDB.XX.0」は、Windowsインストーラー形式(MSI)のOfficeをインストールすれば利用できるようになります。
それ以外の方法では、「Microsoft Access データベース エンジン 再頒布可能コンポーネント」などで別途インストールする必要があります。

Accessランタイムのインスールでも可能です。

クイック実行形式のOfficeの場合

クイック実行形式は Office 2013から採用され、Office 365(Office 2019)ではクイック実行形式のみがリリースされました。

クイック実行形式の Office 製品をインストールした際に、 Office に付属している ODBC ドライバや OLEDB に関するファイルやレジストリ情報が Windows インストーラー形式をインストールした時とは異なる形で登録されます。
64bit MS-Access 2019をインストールしているのに、SQL-ServerでACE.OLEDB.12.0が使えないピンチ!!

クイック実行形式の Office をインストールすると ODBC/OLEDB が利用できないので、「Microsoft Access データベース エンジン 再頒布可能コンポーネント」などで別途インストールが必要となります。

ACEの32/64ビットの共存

Officeは32bitと64bitのサイドバイサイドインストール(共存)はできませんが、「Microsoft Access データベース エンジン」は32/64ビットの共存は可能です。

64bit版がインストール済みの場合

「Microsoft Access データベース エンジン 2016 再頒布可能コンポーネント」の32bit版をダウンロード(英語版のみ)して、コマンドプロンプト上で[「/quit」コマンドを使用してインストールします。

AccessDatabaseEngine.exe /quiet

32bit版がインストール済みの場合

「Microsoft Access データベース エンジン 2016 再頒布可能コンポーネント」の64bit版をダウンロード(英語版のみ)して、コマンドプロンプト上で[「/quit」コマンドを使用してインストールします。

AccessDatabaseEngine_x64.exe /quiet

もし、これでダメなら下記の方法を試して見てください。

AccessDatabaseEngine_X64.exeからaceredist.msiを抽出し、aceredist.msiを実行します。

AccessDatabaseEngine_X64.exe / extract:c:\ adbe2016_x64
msiexec /ic:\adbe2016_x64\aceredist.msi/passive
MSIコマンド コマンドの説明
quiet サイレント・モード、ユーザーとの対話はない
passive 無人モード - 進行状況バーのみ表示

Officeの共存

Officeでは同じ内部バージョンでは共存はできないが、別内部バージョンなら制限はないです。
別内部バージョンでもクイック実行形式では共存できない。

例としてOffice 365の64bitとOffice 2010の32bitは共存可能。
Office 365の64bitに、Access 2010 Runtimeの32bitはインスールすることができる。

Office リリース バージョン インストール テクノロジ
Office 365(Office 2019) 16.0 クイック実行
Office 2016 16.0 クイック実行
Office 2013 15.0 クイック実行
Windows インストーラー (MSI)
Office 2010 14.0 Windows インストーラー (MSI)

最後に

新しくアプリケーションを作るならローカルDBとしてmdbやaccdbとか使用しないで「sqlite」を使用します。Excelファイルを読むにしてもEEPlusやNPOIやClosedXmlを使用します。
昔に作ったアプリケーションの改善作業というしがらみがあって、「Microsoft.ACE.OLEDB」を使うみたいなことになるんでよね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityメモ 移動する床とプレイヤー 親子関係

プレイヤーのスクリプト
レイでの判定分け レイヤー使用

void OnCollisionStay(Collision col)
{

    if (Physics.Linecast(m_charaRay.position, m_charaRay.position + Vector3.down, LayerMask.GetMask("Ground")))
    {
        m_isGroundCollider = true;
    }
    else if(Physics.Linecast(m_charaRay.position, m_charaRay.position + Vector3.down,LayerMask.GetMask("MoveGround")))
    {
        m_isGroundCollider = true;
        gameObject.transform.SetParent(col.transform);//親子関係を設定
    }
    else
    {
        m_isGroundCollider = false;
        gameObject.transform.SetParent(null);//親子関係を外す
    }


}

注意点
プレイヤーでの移動方法で挙動が変わってくるので現在は
transform.positionを操作している

    Vector3 moveDir = Vector3.zero;

    if (Input.GetKey(KeyCode.W))
    {
        moveDir += forwardDir;
    }
    if (Input.GetKey(KeyCode.S))
    {
        moveDir -= forwardDir;
    }
    if (Input.GetKey(KeyCode.D))
    {
        moveDir.z += m_moveSpeed;
    }
    if (Input.GetKey(KeyCode.A))
    {
        moveDir.z -= m_moveSpeed;
    }

    //トランスフォームで移動させたら移動する床でも子オブジェクト状態でも移動できる
    if (moveDir.sqrMagnitude > Mathf.Epsilon)
    {

        transform.position += moveDir;
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む