Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9559

D言語からC++/CLIを経由して.NET APIを利用する(旧暦、和暦処理)

$
0
0

はじめに

D言語から.NET APIを利用したい場合に、どうすればよいかを考えてみました。
何かの参考になればと思います。

やりたいこと

前回の記事で、D言語でのカレンダー表示プログラムを実装しました。
引き続き、旧暦カレンダーを実装してみたいと思います。
.NET APIには、JapaneseCalendarクラスやJapaneseLunisolarCalendarクラスが用意されており、これらを使えば、旧暦や和暦の計算を一から実装せずに済みそうです。

D言語から.NET API呼び出しの実現方法

私が調べた限り、C++/CLIラッピングを経由する方法が簡単だと思いました。
今回はこの方法を採用します。

※参考までに、記事を書くために調べた情報リンクを最後に掲載します。
 C#でDLLを作成し、D言語から呼び出す方法です。

前準備1(C#での実装)

D言語で実装する前に、C#で旧暦や和暦を取得する処理を実装しました。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。

JCalendar.cs
usingSystem;usingSystem.Globalization;publicclassJCalendar{staticstring[]sEra=// 元号{"","明治","大正","昭和","平成","令和"};staticstring[]sRokuyo=// 六曜{"大安","赤口","先勝","友引","先負","仏滅"};staticstring[]sKanshi=// 天干{"","甲","乙","丙","丁","戊","己","庚","辛","壬","癸"};staticstring[]sChishi=// 地支{"","子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"};publicstaticvoidMain(string[]args){DateTimenewDate;if(args.Length>=3){newDate=newDateTime(int.Parse(args[0]),int.Parse(args[1]),int.Parse(args[2]));}else{newDate=DateTime.Now;}Console.WriteLine("西暦 : {0:d}",newDate);if(newDate>=newDateTime(1868,9,8)){printJapaneseCalendar(newDate);}if(newDate>=newDateTime(1960,1,28)&&newDate<=newDateTime(2050,1,22)){printJapaneseLunisolarCalendar(newDate);}}staticvoidprintJapaneseCalendar(DateTimenewDate){JapaneseCalendarjc=newJapaneseCalendar();intera=jc.GetEra(newDate);intyear=jc.GetYear(newDate);intmonth=jc.GetMonth(newDate);intday=jc.GetDayOfMonth(newDate);DateTimejDate=newDateTime(year,month,day);Console.WriteLine("和暦 : {0} {1:d}",sEra[era],jDate);}staticvoidprintJapaneseLunisolarCalendar(DateTimenewDate){JapaneseLunisolarCalendarjlc=newJapaneseLunisolarCalendar();intera=jlc.GetEra(newDate);intyear=jlc.GetYear(newDate);intmonth=jlc.GetMonth(newDate);intday=jlc.GetDayOfMonth(newDate);//閏月を取得stringsLeap="";if(year>0){intleapMonth=jlc.GetLeapMonth(year,era);if(month==leapMonth){sLeap="(閏月)";}//閏月含む場合の月を補正if((leapMonth>0)&&(month>=leapMonth)){month=month-1;//旧暦月の補正}}// 干支(天干、地支)intsy=jlc.GetSexagenaryYear(newDate);inttk=jlc.GetCelestialStem(sy);intts=jlc.GetTerrestrialBranch(sy);// 六曜(大安・赤口・先勝・友引・先負・仏滅)// (月 + 日) % 6 の余りintrokuyo=(month+day)%6;Console.WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}",sEra[era],year,month,day,sLeap);Console.WriteLine("干支 : {0}{1}",sKanshi[tk],sChishi[ts]);Console.WriteLine("六曜 : {0}",sRokuyo[rokuyo]);}}

Window10には、C#コンパイラ(csc.exe)が標準インストールされています。
どのバージョンがどのフォルダにインストールされているかは、それぞれの環境に依存します。
私の環境でのコンパイル、実行例を例示します。
旧暦には、うるう月(閏月)というものが存在します。GetLeapMonthを使って、その年のうるう月を算出し、月を補正します。
実行結果では、月を補正して正しく表示できています。※旧暦カレンダーで検証
干支(天干、地支)は、その年の干支を取得しています。月や日にも干支が存在するそうです。

実行結果
d:\Dev>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe JCalendar.cs
Microsoft (R) Visual C# Compiler version 4.8.3752.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

d:\Dev>JCalendar
西暦 : 2020/09/26
和暦 : 令和 0002/09/26
旧暦 : 令和 0002/08/10
干支 : 庚子
六曜 : 大安

d:\Dev>JCalendar 2020 5 23
西暦 : 2020/05/23
和暦 : 令和 0002/05/23
旧暦 : 令和 0002/04/01 (閏月)
干支 : 庚子
六曜 : 仏滅

前準備2(C++/CLIでの実装)

次に、C++/CLIで旧暦や和暦を取得する処理を実装しました。
C++/CLIに関する日本語での情報は、それほど多くない印象なので、参考になればと思います。
前準備1のC#のソースコードを移植したイメージです。
実行時にパラメータなしだと今日の日付の旧暦や和暦、年月日パラメータを指定するとその日付の旧暦や和暦を表示します。

C++/CLI特有のものとして、演算子 (^)があります。

ハンドル宣言子( ^ は "hat") は、オブジェクトがアクセス不可能であるとシステムが判断したときに、宣言されたオブジェクトが自動的に削除されることを意味する型指定子をに変更します。

オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)より引用

また、.NET APIのクラス生成では、gcnewを使います。

マネージド型 (参照型または値型) のメモリは gcnew によって割り当てられ、ガベージ コレクションによって解放されます。

ref new、gcnew (C++/CLI および C++/CX)より引用

JCal.cpp
usingnamespaceSystem;usingnamespaceSystem::Globalization;#include <string>
String^getEraStr(intera){// 元号array<String^>^sEra=gcnewarray<String^>{"","明治","大正","昭和","平成","令和"};return(sEra[era]);}String^getRokuyoStr(introkuyo){// 六曜array<String^>^sRokuyo={"大安","赤口","先勝","友引","先負","仏滅"};return(sRokuyo[rokuyo]);}String^getKanshiStr(intkanshi){// 天干array<String^>^sKanshi={"","甲","乙","丙","丁","戊","己","庚","辛","壬","癸"};return(sKanshi[kanshi]);}String^getChishiStr(intchishi){// 地支array<String^>^sChishi={"","子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"};return(sChishi[chishi]);}voidprintJapaneseCalendar(DateTimenewDate){JapaneseCalendar^jc=gcnewJapaneseCalendar();intera=jc->GetEra(newDate);intyear=jc->GetYear(newDate);intmonth=jc->GetMonth(newDate);intday=jc->GetDayOfMonth(newDate);DateTimejDate=DateTime::DateTime(year,month,day);Console::WriteLine("和暦 : {0} {1:d}",getEraStr(era),jDate);}voidprintJapaneseLunisolarCalendar(DateTimenewDate){JapaneseLunisolarCalendar^jlc=gcnewJapaneseLunisolarCalendar();intera=jlc->GetEra(newDate);intyear=jlc->GetYear(newDate);intmonth=jlc->GetMonth(newDate);intday=jlc->GetDayOfMonth(newDate);//閏月を取得String^sLeap="";if(year>0){intleapMonth=jlc->GetLeapMonth(year,era);if(month==leapMonth){sLeap="(閏月)";}//閏月含む場合の月を補正if((leapMonth>0)&&(month>=leapMonth)){month=month-1;//旧暦月の補正}}// 干支(天干、地支)intsy=jlc->GetSexagenaryYear(newDate);inttk=jlc->GetCelestialStem(sy);intts=jlc->GetTerrestrialBranch(sy);// 六曜(大安・赤口・先勝・友引・先負・仏滅)// (月 + 日) % 6 の余りintrokuyo=(month+day)%6;Console::WriteLine("旧暦 : {0} {1:d4}/{2:d2}/{3:d2} {4}",getEraStr(era),year,month,day,sLeap);Console::WriteLine("干支 : {0}{1}",getKanshiStr(tk),getChishiStr(ts));Console::WriteLine("六曜 : {0}",getRokuyoStr(rokuyo));}intmain(intargc,char*argv[]){DateTimenewDate=DateTime::Now;if(argc>3){newDate=DateTime::DateTime(atoi(argv[1]),atoi(argv[2]),atoi(argv[3]));}Console::WriteLine("西暦 : {0:d}",newDate);if(newDate>=DateTime::DateTime(1868,9,8)){printJapaneseCalendar(newDate);}if(newDate>=DateTime::DateTime(1960,1,28)&&newDate<=DateTime::DateTime(2050,1,22)){printJapaneseLunisolarCalendar(newDate);}}

私の環境には、Visual C++ ビルドツール 2019がインストールされています。
※参考:Visual C++ ビルドツール 2019 インストール手順

VS2019用 x64 Native Tools コマンドプロンプトを起動します。
共通言語ランタイム (CLR)機能を使用するために、コンパイルオプション/clrをつけてコンパイルします。

旧暦としては、令和は3月27日から始まるようです。※旧暦カレンダーで検証

実行結果
d:\Dev>cl /clr JCal.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314
Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0
Copyright (C) Microsoft Corporation.  All rights reserved.

JCal.cpp
Microsoft (R) Incremental Linker Version 14.24.28314.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:JCal.exe
JCal.obj

d:\Dev>JCal.exe 2019 4 30
西暦 : 2019/04/30
和暦 : 平成 0031/04/30
旧暦 : 平成 0031/03/26
干支 : 己亥
六曜 : 仏滅

d:\Dev>JCal.exe 2019 5 1
西暦 : 2019/05/01
和暦 : 令和 0001/05/01
旧暦 : 令和 0001/03/27
干支 : 己亥
六曜 : 大安

D言語、C++/CLIラッピングのソースコード

前準備が長くなりましたが、ここからが本題です。
.NET APIを呼び出すためのC++/CLIラッピング処理の実装例です。
旧暦、和暦の取得結果をstruct JCALにセットするシンプルな関数です。
D言語から呼び出せるように、extern "C" __declspec(dllexport)属性を付与しています。

JCalDll.cpp
usingnamespaceSystem;usingnamespaceSystem::Globalization;typedefstruct{intera;intyear;intmonth;intday;intleapMonth;intzodiac;intkanshi;intchishi;introkuyo;}JCAL;#define VC_DLL_EXPORTS extern "C" __declspec(dllexport)
VC_DLL_EXPORTSvoid__cdeclgetJapaneseCalendar(intyear,intmonth,intday,JCAL&jcal){DateTimenewDate=DateTime::DateTime(year,month,day);JapaneseCalendar^jc=gcnewJapaneseCalendar();jcal.era=jc->GetEra(newDate);jcal.year=jc->GetYear(newDate);jcal.month=jc->GetMonth(newDate);jcal.day=jc->GetDayOfMonth(newDate);}VC_DLL_EXPORTSvoid__cdeclgetJapaneseLunisolarCalendar(intyear,intmonth,intday,JCAL&jcal){DateTimenewDate=DateTime::DateTime(year,month,day);JapaneseLunisolarCalendar^jlc=gcnewJapaneseLunisolarCalendar();jcal.era=jlc->GetEra(newDate);jcal.year=jlc->GetYear(newDate);jcal.month=jlc->GetMonth(newDate);jcal.day=jlc->GetDayOfMonth(newDate);jcal.leapMonth=0;//閏月を取得if(jcal.year>0){intleapMonth=jlc->GetLeapMonth(jcal.year,jcal.era);if(jcal.month==leapMonth){jcal.leapMonth=1;}//閏月含む場合の月を補正if((leapMonth>0)&&(jcal.month>=leapMonth)){jcal.month=jcal.month-1;//旧暦月の補正}}// 干支(天干、地支)jcal.zodiac=jlc->GetSexagenaryYear(newDate);jcal.kanshi=jlc->GetCelestialStem(jcal.zodiac);jcal.chishi=jlc->GetTerrestrialBranch(jcal.zodiac);// 六曜(大安・赤口・先勝・友引・先負・仏滅)// (月 + 日) % 6 の余りjcal.rokuyo=(jcal.month+jcal.day)%6;}

次にD言語C++/CLIラッピングを呼び出し、旧暦、和暦を表示するプログラムの実装例です。
取得したい情報をstructで定義、メモリを確保しC++/CLIに渡しています。
pragma(lib, "JCalDll")extern (Windows)~を書くだけで、C++/CLI側の関数を呼び出せるので、思っていたより簡単に実装できました。

oldcal.d
importstd.algorithm;importstd.conv;importstd.datetime;importstd.format;importstd.range;importstd.stdio;importcore.sys.windows.windows;structJCAL{intera;intyear;intmonth;intday;intleapMonth;intzodiac;intkanshi;intchishi;introkuyo;}pragma(lib,"JCalDll");extern(Windows)nothrow@nogc{voidgetJapaneseCalendar(int,int,int,refJCAL);voidgetJapaneseLunisolarCalendar(int,int,int,refJCAL);}string[]sEra=["","明治","大正","昭和","平成","令和"];string[]sKanshi=["","甲","乙","丙","丁","戊","己","庚","辛","壬","癸"];string[]sChishi=["","子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"];string[]sRokuyo=["大","赤","勝","友","負","仏"];//  [ "大安", "赤口", "先勝", "友引", "先負", "仏滅" ];voidmain(string[]args){Datedt=Clock.currTime().to!Date;if(args.length>2){dt=Date(args[1].to!int,args[2].to!int,1);}else{dt=Date(dt.year,dt.month,1);}writef("\n%4d年 %2d月",dt.year,dt.month);JCALjcal;stringline1="     |".cycle.take(dt.dayOfWeek*6).to!string;stringline2="      |".cycle.take(dt.dayOfWeek*7).to!string;with(jcal){getJapaneseCalendar(dt.year,dt.month,1,jcal);writef("(%s %2d年) ",sEra[era],year);getJapaneseLunisolarCalendar(dt.year,dt.month,1,jcal);writefln("[%s%s]",sKanshi[kanshi],sChishi[chishi]);writefln("%-(  %s  |%)",["日","月","火","水","木","金","土"]);for(intd=1;d<=dt.daysInMonth;d++){getJapaneseLunisolarCalendar(dt.year,dt.month,d,jcal);line1~=format("%2d %s |",d,sRokuyo[rokuyo]);line2~=format("%2d/%2d%s|",month,day,(leapMonth==1)?"*":" ");}}intnum=(cast(int)line2.length/(7*7))+1;stringline0="-".cycle.take(num*7*7).to!string;roundRobin(line0.chunks(7*7),line1.chunks(6*7),line2.chunks(7*7)).each!(s=>writefln("%s",s));}

D言語ソースコードの補足説明

実行時にパラメータなしだと今日の年月で処理します。argsパラメータで年月を指定することも可能です。

string line1には、全角文字が各日付ごとに1文字含まれます。このため1日の表示幅が6文字となります。
新暦の日付で2文字、半角スペース1文字、六曜表示または全角スペースで1文字、半角スペース1文字、|の1文字の順で合計6文字です。

string line2はすべて半角で、1日の表示幅が7文字となります。
旧暦の年月で5文字、半角スペースまたはうるう月は*で1文字、|の1文字の順で合計7文字です。

string line0は、横線-です。

line0 line1 line2を7日ごとに区切って改行するためにchunks、交互に表示するためにroundRobinを使います。
chunks
roundRobin

コンパイル

VS2019用 x64 Native Tools コマンドプロンプトを起動します。
C++/CLIコンパイル時に、オプション/clr/LDを付けます。JCalDll.libJCalDll.dllが生成されます。
JCalDll.libは、D言語ソースのコンパイル時に必要です。JCalDll.dllは、oldcal.exe実行時に必要です。

dmdでのコンパイル時は、64bitコード生成のためにオプション-m64をつけます。
ldc2をインストールしていれば、ldc2 oldcal.dでもOKです。

コンパイル
d:\Dev>cl /clr /LD JCalDll.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.24.28314
Microsoft (R) .NET Framework の場合 バージョン 4.08.4220.0
Copyright (C) Microsoft Corporation.  All rights reserved.

JCalDll.cpp
Microsoft (R) Incremental Linker Version 14.24.28314.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:JCalDll.dll
/dll
/implib:JCalDll.lib
JCalDll.obj
   ライブラリ JCalDll.lib とオブジェクト JCalDll.exp を作成中

d:\Dev>dmd -m64 oldcal.d

実行結果

表示される日本語の文字コードはUTF-8なので、先にchcp 65001を実行してください。
Shift-JISで日本語出力したい場合は、以前紹介した通り、文字コード変換処理をソースコードに加える必要があります。
紹介情報1
紹介情報2

私の環境のコマンドプロンプトでは、Cicaフォントを使っています。
このため、実行結果では全角スペースが四角の枠で表示されています。

これで、自分の誕生月の旧暦カレンダーも見られそうです。
ただし、JapaneseLunisolarCalendarクラスのGetLeapMonthの仕様で、西暦1960年1月27(旧暦の1959年)以前のうるう月を取得しようとするとExceptionが発生するため、西暦1960年2月以降の旧暦カレンダーしか表示できません。上限は西暦2049年12月です。
無題.png

今回使用した参考情報

JapaneseCalendarクラス
JapaneseLunisolarCalendarクラス
オブジェクト演算子 (^) へのハンドル (C++/CLI および C++/CX)
ref new、gcnew (C++/CLI および C++/CX)

C#のメソッドをC++から呼ぶ方法
C#をC++/CLIでラップしてC++アプリから呼ぶ

六曜・月齢・旧暦カレンダー
年・月・日の干支
六曜はどのように決まるのか?六曜の計算方法は?
c#で六曜の計算

今回使用していない情報だけど参考になりそうな情報

C++/CLIラッピング入門
C++のモジュールからC#のDLLを呼び出してみる
C++からC# DLLを直接利用する方法

Calling-NET-from-D


Viewing all articles
Browse latest Browse all 9559

Trending Articles