TPL + DLR =マルチスレッドスクリプト

「TPL」タスク䞊列ラむブラリず「DLR」動的蚀語ランタむムを孊びたいずずっず思っおいたした。 このために、具䜓的で、できれば非垞に関連性の高いタスクが必芁でした。 私の翻蚳の1぀では 、いわゆる「ゲヌムサむクル」に぀いお説明したした。 そこで議論されたトピック自䜓は私にずっお非垞に興味深いものであり、その䞊、TPL + DLRバンドルがそのタスクに最も適しおいるず考えおいたす。 そこで、軜量の非同期スクリプト゚ンゞンを実装するずいうアむデアを思い付きたした。これは、ゲヌムを含むさたざたなアプリケヌションに簡単にねじ蟌むこずができたす。 ゚ンゞンコアをCで実装するこずにしたした。 私の堎合、動的蚀語の遞択は成り立ちたせんでした。 これらの目的のために、私は長い間Rubyを遞択しおいたす。 しばらくの間、私は時々アむデアを育おたした。



問題の声明



そのため、耇数のスクリプトを非同期に、たたは特定のスレッドで実行する機䌚が欲しいです。 これを行うには、次のタスクを担圓する特定のサヌビスが必芁です。



䞀般的に、このようなもの



そこには䜕が描かれおいたすか ルヌトノヌド-" IRE.Instance "-は、スクリプトの動䜜を調敎するサヌビスそのものの圹割を果たすシングルトンです。 これを行うために、蟞曞はシングルトンむンスタンスに栌玍され、各タスクの゚ントリはRunScriptWithSchedulerおよびRunScriptAsync関数を介しお远加されたす 。 名前が瀺すように、これらの機胜はスケゞュヌラによっお異なり、その制埡䞋でタスクが起動されたす。 「RunScriptWithScheduler」を䜿甚するず、たずえばGUIスレッドでタスクを実行できたす。 " WriteMessage "および " WriteError " メ゜ッドはどこからでもスクリプトを含む利甚でき、メッセヌゞログぞの出力を目的ずしおいたす。 スクリプトの偎では、テキストをサヌビスログにリダむレクトするために、コン゜ヌルぞの暙準出力メ゜ッドをオヌバヌラむドできたす。

サヌビスタスクディクショナリの゚ントリは䜕になりたすか キヌは、実行䞭のスクリプトを識別する䞀意のGUIDです。 このGUIDは、スクリプトの実行を芁求したパヌティに簡単に転送でき、スクリプト自䜓のコンテキストに埋め蟌むこずができたす。 レコヌド倀には、少なくずもCancelationToukenSourceおよびTaskぞの参照を栌玍する必芁がありたす。 Taskスクリプトの䜜成に䜿甚されるCancelationToukenSourceを䜿甚するず、い぀でも終了する必芁があるこずをタスクに通知できたす。 タスク自䜓ぞのリンクにより、Continuationを継続できたすここではTPLに぀いお詳しく説明したせん。



IREサヌビス



おそらく、゚ンゞンのコア、぀たりシングルトンの圢匏で䜜成されたサヌビスの説明から始めたしょう。 シングルトン実装は、 MSDNから取埗されたす 。 たた、JAVAのさたざたなシングルトン実装のかなり詳现な分析ぞのリンクもありたす。 ただ読んでいない人は読むこずを匷くお勧めしたす。

そのため、サヌビスをマルチスレッドの「ダブルチェックロック」シングルトンずしお実装したした。 基本実装コヌドは次のようになりたす。

シングルトンコメントコヌド
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using IronRuby; using Microsoft.Scripting; using Microsoft.Scripting.Hosting; using System.IO; using System.Threading.Tasks; using System.Diagnostics; using Microsoft.Scripting.Runtime; namespace IREngine { public sealed class IRE { #region Singleton private static volatile IRE _instance; private static readonly object SyncRoot = new Object(); private IRE() { //     //        IRE.Instance //  ,       -  ( , )   // var instance = IRE.Instance; } public static IRE Instance { get { if (_instance == null) { lock (SyncRoot) { if (_instance == null) _instance = new IRE(); } } return _instance; } } #endregion #region Consts //     #endregion #region Fields //     #endregion #region Properties //      #endregion #region Private Methods //       #endregion #region Public Methods //        #endregion } }
      
      







構造はコメントから明らかだず思いたす。 コヌドで説明する特別なものはありたせん。 基本的にシングルトンのコメントを2、3だけ付けたす。 シングルトンの代わりに、静的ヘルパヌを䜿甚できたす。 しかし、シングルトンラむフサむクルは制埡が容易です。 たずえば、静的コンストラクタヌの呌び出しの瞬間は非決定的であり、非静的コンストラクタヌの呌び出しはむンスタンスぞの最初のアクセスでのみ発生したす。 これにより、シングルトンを開始する前に将来䜕かを初期化する必芁がある堎合、比范的安党に感じるこずができたす。 もう1぀の興味深い点は、「null」の二重チェックです。 2぀のスレッドは同時に最初の条件内に存圚できたすが、「ロック」はそのうちの1぀だけで実行されたす。 2番目のスレッドは、最初のスレッドがクリティカルセクションを終了するたで埅機したす。 クリティカルセクションを解攟した埌、2番目のスレッドはむンスタンスの「null」を再床チェックする必芁がありたす。 したがっお、䞊列実行スレッドでシングルトンの2぀のむンスタンスを䜜成する状況は蚱可されたせん。



TPLの䜿甚に぀いお簡単に



次に、TPLの基本に぀いお簡単に説明したす。 CodeProjectで詳现を読むこずができたす。

最初に䜜成する非同期タスクは、コン゜ヌル出力バッファヌの監芖です。 最初に曞いたように、サヌビスの機胜の1぀は、コン゜ヌルぞのメッセヌゞず゚ラヌのロギングです。 それで、䜕が必芁ですか



シングルトンのプラむベヌトコンストラクタヌにアクションの初期化を配眮したしょう

初期化アクション
 private IRE() { //     Ruby _outStringBuilder = new StringBuilder(); _errStringBuilder = new StringBuilder(); _outWatchAction = () => { int i = 0; while (IsConsoleOutputWatchingEnabled) { string msg = string.Format("***\t_outWatchTask >> tick ({0})\t***", i++); Debug.WriteLine(msg); WriteMessage(msg); Task.Factory.CancellationToken.ThrowIfCancellationRequested(); int currentLength = OutputBuilder.Length; if (OutputUpdated != null && currentLength != _lastOutSize) { OutputUpdated.Invoke(_outWatchTask, new StringEventArgs(OutputBuilder. ToString(_lastOutSize, currentLength - _lastOutSize))); _lastOutSize = currentLength; } Thread.Sleep(TIME_BETWEEN_CONSOLE_OUTPUT_UPDATES); } }; _outWatchExcHandler = (t) => { if (t.Exception == null) return; Instance.WriteError( string.Format( "!!!\tException raised in Output Watch Task\t!!!\n{0}", t.Exception.InnerException.Message)); }; _errWatchAction = () => { int i = 0; while (IsConsoleErrorWatchingEnabled) { string msg = string.Format("***\t_errWatchTask >> tick ({0})\t***", i++); Debug.WriteLine(msg); WriteError(msg); Task.Factory.CancellationToken.ThrowIfCancellationRequested(); int currentLength = ErrorBuilder.Length; if (ErrorUpdated != null && currentLength != _lastErrSize) { ErrorUpdated.Invoke(_errWatchTask, new StringEventArgs(ErrorBuilder. ToString(_lastErrSize, currentLength - _lastErrSize))); _lastErrSize = currentLength; } Thread.Sleep(TIME_BETWEEN_CONSOLE_OUTPUT_UPDATES); } }; _errWatchExcHandler = (t) => { if (t.Exception == null) return; Instance.WriteError( string.Format( "!!!\tException raised in Error Watch Task\t!!!{0}", t.Exception.InnerException.Message)); }; }
      
      







以䞋は、必芁なプロパティ、定数、およびプラむベヌトフィヌルドの宣蚀です。

フィヌルド、プロパティ、定数の初期化
 #region Consts public readonly int TIME_BETWEEN_CONSOLE_OUTPUT_UPDATES = 1000; #endregion #region Fields private bool _outWatchEnabled; private bool _errWatchEnabled; private Task _outWatchTask; private readonly CancellationTokenSource _outWatchTaskToken = new CancellationTokenSource(); private int _lastOutSize = 0; private Task _errWatchTask; private readonly CancellationTokenSource _errWatchTaskToken = new CancellationTokenSource(); private int _lastErrSize = 0; private readonly StringBuilder _outStringBuilder; private readonly StringBuilder _errStringBuilder; private readonly Action _outWatchAction; private readonly Action<Task> _outWatchExcHandler; private readonly Action _errWatchAction; private readonly Action<Task> _errWatchExcHandler; private readonly CancellationTokenSource _scriptsToken = new CancellationTokenSource(); #endregion #region Properties public StringBuilder OutputBuilder { get { lock (SyncRoot) { return _outStringBuilder; } } } public StringBuilder ErrorBuilder { get { lock (SyncRoot) { return _errStringBuilder; } } } public bool IsConsoleOutputWatchingEnabled { get { lock (SyncRoot) { return _outWatchEnabled; } } set { lock (SyncRoot) { _outWatchEnabled = value; } } } public bool IsConsoleErrorWatchingEnabled { get { lock (SyncRoot) { return _errWatchEnabled; } } set { lock (SyncRoot) { _errWatchEnabled = value; } } } public event EventHandler<StringEventArgs> OutputUpdated; public event EventHandler<StringEventArgs> ErrorUpdated; #endregion
      
      







このコヌドは䜕をしたすか 定期的に、StringBuilderの長さが比范され、長さが倉曎されるず、察応するむベントがプルされたす。 このむベントのハンドラヌGUI偎は、远加されたメッセヌゞテキストを衚瀺したす。 GUI偎では、ハンドラ内のコヌドもタスク内で実行されたすが、GUIから受信したタスクスケゞュヌラの明瀺的な指瀺があるこずに泚意しおください。 なぜなら GUIスレッドで実行されない非同期タスクではむベントがぎくぎく動くため、ハンドラヌのコヌドはむンタヌフェヌス芁玠に盎接アクセスできたせん。 メむンアプリケヌションりィンドりのスケゞュヌラを明瀺的に指定しお、アクションを䜜成し、むンタヌフェむススレッドで実行する必芁がありたす。 以䞋は、GUI偎のむベントハンドラヌのコヌドです。

むベントハンドラヌコヌド
 IRE.Instance.OutputUpdated += (s, args) => { string msg = string.Format("***\tIRE >> Out Updated callback\t***\nResult: {0}\n***\tEND\t***\n",args.Data); Debug.WriteLine(msg); var uiUpdateTask = Task.Factory.StartNew(() => { OutputLogList.Items.Add(msg); }, Task.Factory.CancellationToken, TaskCreationOptions.None, _uiScheduler); uiUpdateTask.Wait(); }; IRE.Instance.ErrorUpdated += (s, args) => { string msg = string.Format("!!!\tIRE >> Err Updated callback\t!!!\nResult: {0}\n!!!\tEND\t!!!\n", args.Data); Debug.WriteLine(msg); var uiUpdateTask = Task.Factory.StartNew(() => { ErrorLogList.Items.Add(msg); }, Task.Factory.CancellationToken, TaskCreationOptions.None, _uiScheduler); uiUpdateTask.Wait(); }; IRE.Instance.StartWatching();
      
      







このコヌドは、コン゜ヌル出力バッファヌの曎新むベントにサブスクリプションを远加したす。 その䞭で泚目すべき䞻なものは「 _uiScheduler 」です。 これは、メむンりィンドりのデザむナヌで䜜成されたりィンドりスケゞュヌラヌぞのリンクです。 このリンクは次のように䜜成されたす。

 public MainWindow() { InitializeComponent(); _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); }
      
      





このスケゞュヌラで䜜成されたすべおのタスクは、むンストヌルされおいるスレッドに関係なく、GUIスレッドで起動されたす。 グラフィカルむンタヌフェむス芁玠ぞの短絡は、クロススレッドアクセス䟋倖を匕き起こしたせん。

「 uiUpdateTask.Wait; 」ずいう行に぀いお。 " try-catch "でこの行をフレヌム化するこずが望たしいです。 タスク内で発生するすべおの䟋倖は、タスクを䜜成したスレッドにすぐには転送されたせん。 呌び出しスレッドで䟋倖にアクセスする1぀の方法は、Wait関数を呌び出すこずです。 タスクに「継続」を远加しお、その䞭に䟋倖を投皿するこずもできたす。 方法は関係ありたせんが、䟋倖を凊理する必芁がありたす。 それ以倖の堎合、「GarbageCollector」がタスクに到達するず、䟋倖が呌び出しスレッドに送信されたす。 どの時点でこれが起こるかは䞍明です。 したがっお、アプリケヌションにずっおこれは臎呜的です。

この堎合、簡単にするために、「 try-catch 」は远加したせんでしたが、おそらく埌で远加したす。 ここでのコヌドは非垞に単玔ですが、将来はすべおが倉曎される可胜性がありたす。

コン゜ヌル出力を監芖するためのタスクを䜜成するためのコヌドを提䟛するこずは残りたす。

 public void StartWatching() { StopWatching(); if (_outWatchTask != null) _outWatchTask.Wait(); if (_errWatchTask != null) _errWatchTask.Wait(); IsConsoleOutputWatchingEnabled = IsConsoleErrorWatchingEnabled = true; _outWatchTask = Task.Factory.StartNew(_outWatchAction, _outWatchTaskToken.Token); _outWatchTask.ContinueWith(_outWatchExcHandler, TaskContinuationOptions.OnlyOnFaulted); _errWatchTask = Task.Factory.StartNew(_errWatchAction, _errWatchTaskToken.Token); _errWatchTask.ContinueWith(_errWatchExcHandler, TaskContinuationOptions.OnlyOnFaulted); }
      
      





ここのすべおも簡単です。 念のため、远跡タスクを停止したす " StopWatching; "。 この関数を呌び出すず、単玔に「 IsConsoleOutputWatchingEnabled 」および「 IsConsoleErrorWatchingEnabled 」 フラグが falseに蚭定され、 「 CancelationToken 」のブレヌクが停止されたす。 トヌクンだけに制限するこずができたす。 ただし、䞀般に、トヌクンを介した停止芁求は緊急停止ず芋なされたす。 特別な関数 " Task.Factory.CancellationToken.ThrowIfCancellationRequested; "もありたす。この関数の呌び出しにより、トヌクンを介したタスクの実行䞭にキャンセル芁求が受信された堎合、タスクは䟋倖をスロヌしたす。 この関数を呌び出しお、䞀般にCancelationTokenのステヌタスを確認するのは非垞にコストのかかる手順です。 したがっお、できる限りめったに行わないこずが望たしい。

次に泚意したいのは、「 _outWatchTask.ContinueWith_outWatchExcHandler、TaskContinuationOptions.OnlyOnFaulted; 」ずいう圢匏の構築です。 このコヌドは、いわゆる「継続」継続ずいうタスクにハングアップしたす。 さらに、オプション「 TaskContinuationOptions.OnlyOnFaulted 」で継続が䜜成されたす。 このような継続は、タスクの「AggregatedException」に少なくずも1぀の䟋倖が含たれおいる堎合にのみ呌び出されたす。 簡単に蚀えば、タスクが正垞に完了した堎合、この続線は無芖されたす。 たた、いく぀かの䟋倖が存圚する可胜性があるこずにも泚意しおください。 結局、このタスク内にネストされたサブタスクを䜜成できたす。 ネストされたタスクが「 GC 」によっお収集されるず、未凊理の䟋倖はすべお、同じ「AggregatedException」の圢匏で芪にポップアップしたす。 したがっお、ネストされた䟋倖のツリヌ党䜓を取埗できたす。 この䟋倖ツリヌをフラットリストに倉換する特別なメ゜ッド「 AggregatedException.Flatten 」がありたす。



スクリプトの基本



それでは、スクリプト自䜓に぀いお話したしょう。 たず、「ScriptEngine」を䜜成し、アプリケヌションに必芁な.Netアセンブリをロヌドする必芁がありたす。 これは単玔に行われたす

 _defaultEngine = Ruby.CreateEngine((setup) => { setup.ExceptionDetail = true; }); _defaultEngine.Runtime.LoadAssembly(typeof(IRE).Assembly);
      
      





このコヌドをシングルトンプラむベヌトコンストラクタヌの䞊郚に远加したした。 コヌドの最初の郚分は、実際にScriptEngineのむンスタンスを䜜成したす。 パラメヌタヌずしお枡されるラムダを䜿甚するず、゚ンゞンを構成できたす。 これはオプションです。 しかし、このアプロヌチでは、ScriptEngineがRubyコヌドをコンパむルしないようにしお、スクリプトが毎回解釈されるようにするこずができたす。 これは、たずえば、WindowsPhone 7プラットフォヌムで䟿利です。 メモリを節玄したす。 䟋倖に関する情報の詳现な出力を含めたした。

2番目の郚分は、サヌビスを含むアセンブリをRuby゚ンゞンのランタむムに単玔にロヌドしたす。 これがないず、スクリプトからアプリケヌションず通信できなくなりたす。 同様に、他の必芁なアセンブリをロヌドできたす。 䞀般に、このアセンブリの読み蟌みを別の方法で行うこずをお勧めしたす。これにより、新しいアセンブリを簡単に远加でき、同時に同じ皮類のコヌド行のシヌトが目障りになりたせん。

非同期タスクスクリプトずいく぀かのサヌビスメ゜ッドを远加する関数のコヌドを提䟛するこずは残っおいたす。

非同期コヌドタスクスクリプトの远加
 public Guid RunScriptAsync(string code) { var scriptScope = _defaultEngine.CreateScope(); CompiledCode compiledCode = null; try { ScriptSource scriptSource = _defaultEngine.CreateScriptSourceFromString(code, SourceCodeKind.AutoDetect); var errListner = new ErrorSinkProxyListener(ErrorSink.Default); compiledCode = scriptSource.Compile(errListner); } catch (Exception ex) { WriteError(ex.Message); return Guid.Empty; } var action = new Action(() => compiledCode.Execute(scriptScope)); var tokenSource = new CancellationTokenSource(); var task = new Task(action, tokenSource.Token, TaskCreationOptions.LongRunning); var guid = Guid.NewGuid(); AddTask(guid, new TaskRecord { TokenSource = tokenSource, Task = task }); task.Start(); task.ContinueWith((t) => { // Actually we don't needed this due to "TaskContinuationOptions.OnlyOnFaulted" is set if (t.Exception == null) return; t.Exception.Flatten().Handle((ex) => { Instance.WriteError(t.Exception.InnerException.Message); return true; }); }, TaskContinuationOptions.OnlyOnFaulted); task.ContinueWith((t) => { Instance.RemoveTask(guid); }); return guid; } public void AddTask(Guid key, TaskRecord record) { _tasks.Add(key, record); } public void RemoveTask(Guid key) { _tasks.Remove(key); } public Task GetTask(Guid key) { return _tasks.ContainsKey(key) ? _tasks[key].Task : null; } public void RequestCancelation(Guid key) { var tokenSource = _tasks.ContainsKey(key) ? _tasks[key].TokenSource : null; if (tokenSource == null) return; tokenSource.Cancel(); }
      
      





ここには䜕がありたすか たず、ScriptScopeを䜜成したす。 デフォルトのRuby゚ンゞンを䜿甚したすが、各ScriptScopeには独自のスクリプトがありたす。 すべおの倉数ずスクリプト実行結果のスコヌプは、これらのスコヌプに制限されたす。

さらに「try-catch」の内郚でScriptSourceずCompiledCodeが䜜成されたす。 「トラむ-キャッチ」が必芁です コンパむル枈みコヌドの䜜成時に䟋倖が発生する堎合がありたす。 この堎合、空のGuidを返し、呌び出し偎はこの状況を適切に凊理する必芁がありたす。 スクリプトが正垞にコンパむルされたら、スクリプト実行タスクの䜜成を開始できたす。

したがっお、スクリプトを実行するアクションを䜜成し、CancelationTokenSource、オプション「TaskCreationOptions.LongRunning」を持぀新しいタスクを䜜成したす結局、スクリプトは長時間実行される可胜性がありたす。 次に、タスクの新しいGUIDを䜜成し、最埌に、すべおをスクリプト化されたタスクの蟞曞の新しい゚ントリにパックしたす。 最初に蚈画したずおり。

䜜成埌、タスクを実行し、䟋倖凊理を䜿甚しおタスクに継続を添付したす。 タスクの継続では、゚ラヌメッセヌゞをログに曞き蟌んだ埌、AggregatedExceptionを「フラット化」し、すべおの䟋倖を凊理したす trueを返したす 。

䞊蚘の継続に加えお、タスクの完了時に呌び出される別の継続を添付したす。 この続きでは、䜿甚枈みのTaskを蟞曞から削陀したす。 もはや有甚ではありたせん。

サヌビス機胜を䜿甚するず、すべおがシンプルに思えたす。



䜿甚䟋



このスクリプトフレヌムワヌクの動䜜を瀺すために、りィンドりアプリケヌションプロゞェクトを䜜成したした。 りィンドりのレむアりトは簡単です。

 <Window x:Class="IREngineTestBed.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Button x:Name="StartButton" Content="Start" Grid.Row="1" Margin="5" Height="25" Click="StartButton_Click"/> <Button x:Name="StopButton" Content="Stop" Grid.Row="1" Grid.Column="1" Margin="5" Height="25" Click="StopButton_Click"/> <ListBox x:Name="OutputLogList" Grid.Column="0" Margin="5"/> <ListBox x:Name="ErrorLogList" Grid.Column="1" Margin="5"/> </Grid> </Window>
      
      





おわりに



りィンドりには、2぀のListBox'aず2぀のボタンが含たれおいたす。 文字列を远加するパフォヌマンスが䜎いため、TextBlockを䜿甚しおログを衚瀺したせんでしたTextBlock.Text + =“ some string”-ご存知のように、倧きな悪がありたす。 しかし、実際にはリストボックスは悪い考えです。 テキストをコピヌしないでください。 䞀般に、ListBox'yはデモ甚です急いで。 将来的には、RichTextBoxを眮き換えるこずになるでしょう。 圌らは文字列の仮想化を持っおいるようで、テキストはStringBuilderに远加されおいるようです。

そのため、「StartButton」ボタンを䜿甚しお、コン゜ヌル出力バッファずスクリプト実行タスクを監芖するためのタスクの再開始が実行されたす。 「StopButton」ボタン-すべおが停止したす。

テストスクリプトのコヌドは次のずおりです。

 #!ruby19 # encoding: utf-8 include IREngine class IRE def log(message) IRE.Instance.write_message message end def err(message) IRE.Instance.write_error message end end def log(message, is_err = false) IRE.Instance.log message unless is_err IRE.Instance.err message if is_err end 5.times{|i| log "Hello, Output! (from Ruby Async Task)" log "Hello, Error! (from Ruby Async Task)", true } raise "\n!!!\tBOOO! Catch super scaring exception from RUBY...\t!!!\n"
      
      





このコヌドでは、メッセヌゞをログに出力するヘルパヌメ゜ッドがサヌビスクラスに远加されたす。 さらに、シングルトンむンスタンスに远加のメ゜ッドが远加されたす。 しかし、メ゜ッドの名前に接頭蟞「self」たたは「IRE」を付けた堎合、それらは静的メ゜ッドずしお䜜成されたす。 おそらく埌で行いたすこのケヌスを個別にテストする必芁がありたす。

スクリプトの本文には、ログぞの行の5倍の出力ず䟋倖転送が衚瀺されたす。 この䟋倖は、サヌビス偎で正しく凊理されたす。 これは、スクリプトの実行埌のむンタヌフェむスの倖芳です。





おわりに



この蚘事では、耇数のスクリプトを同時に非同期で実行できる゚ンゞンの基本的な実装を玹介したした。 執筆時点では、すべおの蚈画が実装されおいるわけではありたせん。 たずえば、スクリプトの構文゚ラヌの状況の凊理の正確性を確認する必芁がありたす。 たた、特定のスケゞュヌラヌを瀺すスクリプト起動メ゜ッドはただ実装されおいたせん。

githubのリポゞトリでコヌドの開発をフォロヌできたす。 誰かがプロゞェクトの開発に参加しおくれたら嬉しいです。



ご枅聎ありがずうございたした



All Articles