OpenTKとC#でOpenCLプログラミング

C#でお手軽にOpenCLを使ったプログラムを作れないかと思い、OpenTKを使用。

OpenTK 3.xではOpenCLの機能がClooとして独立したようですが、現行のOpenTK 4.xでは再びOpenCLが統合されています。

まずはNugetパッケージマネージャからOpenTK 4.7.xをプロジェクトに追加します。

サンプルプログラムはhttps://github.com/opentk/opentk/tree/master/tests/OpenToolkit.OpenCL.Testsにあるので参照。

using OpenTK.Compute.OpenCL;
using System.Text;
CL.GetPlatformIds(0, null, out uint platformCount);
CLPlatform[] platformIds = new CLPlatform[platformCount];
CL.GetPlatformIds(platformCount, platformIds, out _);
Console.WriteLine(platformIds.Length);
foreach (CLPlatform platform in platformIds)
{
    Console.WriteLine(platform.Handle);
    CL.GetPlatformInfo(platform, PlatformInfo.Name, out byte[] val);
    Console.WriteLine(System.Text.Encoding.ASCII.GetString(val));
}
foreach (IntPtr platformId in platformIds)
{
    CL.GetDeviceIds(new CLPlatform(platformId), DeviceType.All, out CLDevice[] deviceIds);
    CLContext context = CL.CreateContext(IntPtr.Zero, (uint)deviceIds.Length, deviceIds, IntPtr.Zero, IntPtr.Zero, out CLResultCode result);
    if (result != CLResultCode.Success)
    {
        throw new Exception("The context couldn't be created.");
    }
    string code = @"
                __kernel void add(__global float* A, __global float* B,__global float* result, const float mul)
                {
                    int i = get_global_id(0);
                    for (int j =0;j < 60000;j++)
                    {
                        for (int k =0;k < 60000;k++)
                        {
                            result[i] = (A[i] + B[i])*mul;
                        }
                    }
                }";
    CLProgram program = CL.CreateProgramWithSource(context, code, out result);
    CL.BuildProgram(program, (uint)deviceIds.Length, deviceIds, null, IntPtr.Zero, IntPtr.Zero);
    CLKernel kernel = CL.CreateKernel(program, "add", out result);
    int arraySize = 20;
    float[] A = new float[arraySize];
    float[] B = new float[arraySize];
    for (int i = 0; i < arraySize; i++)
    {
        A[i] = 1;
        B[i] = i;
    }
    CLBuffer bufferA = CL.CreateBuffer(context, MemoryFlags.ReadOnly | MemoryFlags.CopyHostPtr, A,
        out result);
    CLBuffer bufferB = CL.CreateBuffer(context, MemoryFlags.ReadOnly | MemoryFlags.CopyHostPtr, B,
        out result);
    float[] pattern = new float[] { 1, 3, 5, 7 };
    CLBuffer resultBuffer = new CLBuffer(CL.CreateBuffer(context, MemoryFlags.WriteOnly,
        new UIntPtr((uint)(arraySize * sizeof(float))), IntPtr.Zero, out result));
    try
    {
        CL.SetKernelArg(kernel, 0, bufferA);
        CL.SetKernelArg(kernel, 1, bufferB);
        CL.SetKernelArg(kernel, 2, resultBuffer);
        CL.SetKernelArg(kernel, 3, -1f);
        CLCommandQueue commandQueue = new CLCommandQueue(
                CL.CreateCommandQueueWithProperties(context, deviceIds[0], IntPtr.Zero, out result));
        CL.EnqueueFillBuffer(commandQueue, bufferB, pattern, UIntPtr.Zero, (UIntPtr)(arraySize * sizeof(float)), null,
            out _);
        //CL.EnqueueNDRangeKernel(commandQueue, kernel, 1, null, new UIntPtr[] {new UIntPtr((uint)A.Length)},
        //	null, 0, null,  out CLEvent eventHandle);
        CL.EnqueueNDRangeKernel(commandQueue, kernel, 1, null, new UIntPtr[] { new UIntPtr((uint)A.Length) },
            null, 0, null, out CLEvent eventHandle);
        CL.Finish(commandQueue);
        CL.SetEventCallback(eventHandle, (int)CommandExecutionStatus.Complete, (waitEvent, data) =>
        {
            float[] resultValues = new float[arraySize];
            CL.EnqueueReadBuffer(commandQueue, resultBuffer, true, UIntPtr.Zero, resultValues, null, out _);
            StringBuilder line = new StringBuilder();
            foreach (float res in resultValues)
            {
                line.Append(res);
                line.Append(", ");
            }
            Console.WriteLine(line.ToString());
        });
        //get rid of the buffers because we no longer need them
        CL.ReleaseMemoryObject(bufferA);
        CL.ReleaseMemoryObject(bufferB);
        CL.ReleaseMemoryObject(resultBuffer);
        //Release the program kernels and queues
        CL.ReleaseProgram(program);
        CL.ReleaseKernel(kernel);
        CL.ReleaseCommandQueue(commandQueue);
        CL.ReleaseContext(context);
        CL.ReleaseEvent(eventHandle);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
        throw;
    }
}

GPU上で実行されるプログラムはC言語で記述できます。

拍子抜けするぐらい簡単にOpenCLを使用したプログラムを作れるんですね。

どうしてもGPUへのデータ転送がボトルネックになりやすいので、高速化するのは中々難しいですが、機会があれば活用したいところ。

SharpZipLibの日本語対応

SharpZipLib 1.2.0以前でtarファイルを作成すると日本語が文字化けしていたけど、何時の間にか日本語に対応していたんですね。TarOutputStreamのConstructorでEncodingを指定できるではないですか。

github(https://github.com/icsharpcode/SharpZipLib)から最新のソースコードをダウンロードしてきて、自らビルドしましょう。

.NET COREでLinuxで動作するサービスを作成する

.NET Core + C#でLinuxのDeamon作れないかなと思ったら、思ったより簡単に作れるようになっていたので覚え書き。Program.csとDaemonService.csの中味はほぼ定型なのでサンプルからそのまま流用します。

以下のサンプルは.NET Core 3.1で記述しています。

// Program.cs

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace DaemonSample
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = new HostBuilder()
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddOptions();
                    services.Configure<DaemonConfig>(hostContext.Configuration.GetSection("Daemon"));

                    services.AddSingleton<IHostedService, DaemonService>();
                })
                .ConfigureLogging((hostingContext, logging) => {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                });

            await builder.RunConsoleAsync();
        }
    }
}
// DaemonService.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace DaemonSample
{
    class DaemonService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private readonly IOptions<DaemonConfig> _config;
        public DaemonService(ILogger<DaemonService> logger, IOptions<DaemonConfig> config)
        {
            _logger = logger;
            _config = config;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Starting daemon: " + _config.Value.DaemonName);
            _logger.LogInformation("arg1:{0}", _config.Value.arg1);
            _logger.LogInformation("arg2:{0}", _config.Value.arg2);

            // ここでサーバー本体の処理をTaskとして起動します。

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Stopping daemon.");
            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _logger.LogInformation("Disposing....");
        }
    }
}
// DaemonConfig.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace DaemonSample
{
    class DaemonConfig
    {
        public string DaemonName { get; set; }
        public string arg1{ get; set; }
        public string arg2{ get; set; }
    }
}

起動時のコマンドラインは下記の通り。コマンドライン引数の受け渡し方が非常に独特です。

/usr/bin/dotnet run --project deamonsample --configuration Release --Daemon:DaemonName="Sample Daemon" Daemon:arg1="argument 1" Daemon:arg2="argument 2"

Demonとして起動するにはsystemdを使用します。/etc/systemd/systemにdeamonsample.serviceファイルを作成し、下記の様に記述します。

[Unit]
Description=deamon sample
After=network.target

[Service]
ExecStart=/usr/bin/dotnet run --project atozipsrv --configuration Release --Daemon:DaemonName="deamonsample " Daemon:arg1="argument 1" Daemon:arg2="argument 2"
WorkingDirectory=/usr/src/deamonsample
Restart=always
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=atozip
Environment=DOTNET_CLI_HOME=/var/src/deamonsample

[Install]
WantedBy=multi-user.target

後は通常のsystemdのDeamon同様に以下のコマンドで起動します。

$ systemctl start deamonsample

参考:Creating a Daemon with .NET Core (Part 1)

Linux上のClosedXMLで画像データを扱うときにSystem.DllNotFoundExceptionが発生する

Linux(Ubuntu 18.04)環境に.NET Core SDK 2.2をインストールし、ClosedXmlで画像を含むExcelファイルを扱おうとしたところ、下記の様な例外が発生して実行出来ません。

System.TypeInitializationException: The type initializer for 'Gdip' threw an exception. ---> System.DllNotFoundException: Unable to load shared library 'libdl' or one of its dependencies. In order to help diagnose loading problems, consider setting th
 e LD_DEBUG environment variable: liblibdl: cannot open shared object file: No such file or directory at Interop.Libdl.dlopen(String fileName, Int32 flag)
     at System.Drawing.SafeNativeMethods.Gdip.LoadNativeLibrary()
     at System.Drawing.SafeNativeMethods.Gdip..cctor()
     --- End of inner exception stack trace ---
     at System.Drawing.SafeNativeMethods.Gdip.GdipLoadImageFromDelegate_linux(StreamGetHeaderDelegate getHeader, StreamGetBytesDelegate getBytes, StreamPutBytesDelegate putBytes, StreamSeekDelegate doSeek, StreamCloseDelegate close, StreamSizeDelegate size, IntPtr& image)
     at System.Drawing.Image.InitFromStream(Stream stream)
     at ClosedXML.Excel.Drawings.XLPicture..ctor(IXLWorksheet worksheet, Stream stream)
     at ClosedXML.Excel.Drawings.XLPictures.Add(Stream stream)
     at ClosedXML.Excel.XLWorkbook.LoadDrawings(WorksheetPart wsPart, IXLWorksheet ws)
     at ClosedXML.Excel.XLWorkbook.LoadSpreadsheetDocument(SpreadsheetDocument dSpreadsheet)
     at ClosedXML.Excel.XLWorkbook.LoadSheets(Stream stream)

.NET Core Runtimeインストール時に依存関係が処理されていないことが原因です。下記のコマンドを実行してGDI+関連のライブラリをインストールすると解決します。

sudo apt install libc6-dev 
sudo apt install libgdiplus
cd /usr/lib
sudo ln -s <a rel="noreferrer noopener" target="_blank" href="http://libgdiplus.so/">libgdiplus.so</a> gdiplus.dll

環境によってはLD_LIBRARY_PATHの設定も必要になるようです。

ASP.NET COREを使ったWEBサービス作成(習作)

習作としてASP.NET COREを使用したWEBサービスを作成してみた。
もちろんLinux(Ubuntu 18.04)上で動作している。

Excelファイルをアップロードすると、適当な文字列部分をバーコード画像に置き換えてくれる。

https://barcode.code-lab.net/

.NET Coreで実行可能ファイルを作る

最近はちょっとしたプログラムを作るときに.NET Coreコンソールアプリケーションを選択するようにしている。今のところ実際にLinuxで動かすような機会はないが、クロスプラットフォームに出来るからだ。ただ通常はDLLとしてビルドされるため、PCに詳しくない人には起動手順を説明しにくい。

第三者向けにインストーラを作成して使用手順を書こうとした時に、ふと.NET Core 2.2からはDLLではなくて、実行ファイル(EXEファイル)としてビルド出来ることを思い出した。第三者向けに配布するなら実行ファイルとしてビルドしておいた方が便利そうだ。

コマンドラインから実行ファイルを生成するのは以下のように”-r win-x64 –self-contained true”指定するだけである。

dotnet publish -c release -r win-x64 --self-contained true

win-x64はRID(Runtime Identifier)でビルドするターゲットとなるプラットフォームを示します。linux-x64とかwin-arm64などと指定すれば異なるOSやCPUアーキテクチャをターゲットとしたバイナリを簡単に作れて便利です。

URL:
https://docs.microsoft.com/ja-jp/dotnet/core/deploying/deploy-with-cli
https://docs.microsoft.com/ja-jp/dotnet/core/rid-catalog

僕の経験した最悪の糞コードの話

ツイッターで久しぶりに糞コードの話を見かけたので、僕の知る最悪の糞コードの話を書いておこうと思う。

そのコードはGeneric Programmingによって実装されていました。クラスはきちんと整理されており、テンプレートを最大限に活用したコードは必要最小限の記述となっています。整然とひとつの思想によって実装された素晴らしいコードでした。いくつかの致命的な問題を除いては・・・

一つ目の致命的な問題は、このプロダクトがWindows向けであり、開発環境にVisual C++を使用していたことです。当時のVC++コンパイラにはバグがありました。複雑なテンプレートを定義すると、コンパイラがジェネラルプロテクションエラーを起こして終了してしまうのです。

知っていれば回避可能な、実用上は問題ないバグをBy Design(仕様)と称していた時代の話です。コンパイラのバグが近日中に修正される可能性はありません。この問題を根本的に解決するには、プログラムの根幹を構成しているGeneric Programmingを、互換性を維持し挙動を変えることなく取り除く・・・・って、ほぼリメイクですヤン!

実際には既にリリースしてしまっているので互換性維持は絶対、修正や機能追加は待ったなし、そんな大規模修正できません。影響の少なさそなところからGeneric Programmingを取り除き、VC++が落ちない程度までテンプレートの使用量を減らしては、修正をするって作業に明け暮れたのでした。

一つ目の・・・と書いたからには二つ目以降もあるわけで、DLLの引数としてクラスを何の考えもなく受け渡してる。それもDLL側で確保して呼び出し元で解放させたり。C++はコードや変数がどのようにメモリ上に展開されるかは仕様として定められていません。したがってクラスを引数として受け渡そうとするなら、全てのモジュールを同じコンパイラ、同じヘッダファイル、同じコンパイルオプションでビルドするのが必須です。ちょっと直すだけでも全モジュールをビルドし直して置き換えないと動作を保証できないという困難さ。

このコードを書いたエンジニア、どうやらJavaのスペシャリストであったようです。JavaのプログラマとしてOOな設計やコーディング技法には精通しており、その技をしっかりと使いこなしていました。でもC++固有の言語仕様には非常に疎く、おそらくはほぼ初心者。newしてもdeleteを呼んでいる場所が全くない。C++の言語仕様上の地雷をことごとく踏み抜いてく、そんなクソースでした。

Windows 7以降にオフライン環境で.NET Framework 3.5をインストールする

Windows 7移行のOSに.NET Framework 3.5をインストールする場合など、コントロールパネルの「Windowsの機能の有効化または無効化」にて機能の追加をする場合、最新のモジュールを試みるために、インターネット接続に制限のある環境ではエラーとなる。

この場合にはコマンドラインからDISMコマンドを実行して、インストールメディアからインストールする事で回避できる。例えばWindwos 7上で.NET Framework 3.5をインストールする場合には、下記のコマンドを実行する。(D:はインストールメディアのあるドライブ)

DISM /Online /Enable-Feature /FeatureName:NetFx3 /Source:D:\sources\sxs\

参考:Deploy .NET Framework 3.5 by using Deployment Image Servicing and Management (DISM)

習作:FacebookのLikeボタンをクリックする

C#でSeleniumを用いて、Facebookのページ内にある「いいね!」を自動的にクリックするサンプル。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using OpenQA.Selenium;

namespace LikeClicker
{
    class Program
    {
        static void Main(string[] args)
        {
            string userID = "test@example.co.jp";
            string password = "password";

            // AppSettings.BrowserName.Firefoxを変更することによって対象のブラウザを変更できます
            using (IWebDriver webDriver = WebDriverFactory.CreateInstance(AppSettings.BrowserName.HeadlessChrome))
            {
                webDriver.Url = @"https://www.facebook.com/";

                IWebElement userIdElement = webDriver.FindElement(By.Id("email"));
                IWebElement passwordElement = webDriver.FindElement(By.Id("pass"));

                userIdElement.SendKeys(userID);
                passwordElement.SendKeys(password);
                userIdElement.Submit();
                Console.WriteLine("認証が終了したら任意のキーを押して下さい。");
                Console.Read();

                string[] followURL = {
                        "https://www.facebook.com/pg/username1/posts/?ref=page_internal",
                        "https://www.facebook.com/pg/username2/posts/?ref=page_internal"};
                while (true)
                {
                    foreach (var tergetURL in followURL)
                    {
                        webDriver.Url = tergetURL;
                        Thread.Sleep(5000);

                        int findCount = 0;
                        foreach (var divElement in webDriver.FindElements(By.ClassName("_37uu")))
                        {
                            foreach (var refElement in divElement.FindElements(By.ClassName("UFILikeLink")))
                            {
                                if (refElement.GetAttribute("class").IndexOf("UFILinkBright") <= 0)
                                {
                                    refElement.Click();
                                    Thread.Sleep(3000);
                                }
                            }
                        }
                    }

                    Thread.Sleep(1000 * 60 * 10);
                }
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.Safari;

namespace LikeClicker
{
    internal class WebDriverFactory
    {
        public static IWebDriver CreateInstance(AppSettings.BrowserName browserName)
        {
            switch (browserName)
            {
                case AppSettings.BrowserName.None:
                    throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));

                case AppSettings.BrowserName.Chrome:
                    return new ChromeDriver();

                case AppSettings.BrowserName.HeadlessChrome:
                    ChromeOptions option = new ChromeOptions();
                    option.AddArgument("--headless");
                    return new ChromeDriver(option);

                case AppSettings.BrowserName.Firefox:
                    FirefoxDriverService driverService = FirefoxDriverService.CreateDefaultService();
                    driverService.FirefoxBinaryPath = @"C:\Program Files (x86)\Mozilla Firefox\firefox.exe";
                    driverService.HideCommandPromptWindow = true;
                    driverService.SuppressInitialDiagnosticInformation = true;
                    return new FirefoxDriver(driverService);

                case AppSettings.BrowserName.InternetExplorer:
                    return new InternetExplorerDriver();

                case AppSettings.BrowserName.Edge:
                    return new EdgeDriver();

                case AppSettings.BrowserName.Safari:
                    return new SafariDriver();

                default:
                    throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LikeClicker
{
    internal static class AppSettings
    {
        public enum BrowserName
        {
            None,
            Chrome,
            HeadlessChrome,
            Firefox,
            InternetExplorer,
            Edge,
            Safari
        }
    }
}

C# ODBC.NETでテーブルを複製するサンプル

DataReaderで取得したフィールド名等の情報を本に、SQL文(DELETE文、INSETR文)を動的に作成して、テーブル内のデータコピーを行うサンプル。

CopyTableの引数としてテーブル名、プライマリキー名の配列、コピー元データベースのConnection、コピー先データベースのConnectionを与えて下さい。

    class Program
    {
        static void Main(string[] args)
        {
            OdbcConnection srceConn = new OdbcConnection("Driver={SQL Server};Server=SV01;UID=example;PWD=;");
            srceConn.Open();

            OdbcConnection destConn = new OdbcConnection("Driver={SQL Server};Server=SV02;UID=example;PWD=;");
            destConn.Open();

            CopyTable("table_name", new string[] { "primary_key_1", "primary_key_2" }, srceConn, destConn);
            Console.ReadKey();
        }

        static void CopyTable(string tableName, string[] keyNames, OdbcConnection srceConn, OdbcConnection destConn)
        {
            Console.WriteLine("Copying {0}", tableName);

            StringBuilder insSql = null;
            Dictionary<string, int> columnIndexs = new Dictionary<string, int>();

            // DELETEコマンド文字列の生成
            StringBuilder delSQL = new StringBuilder();
            delSQL.Append(" DELETE ");
            delSQL.Append(tableName);
            delSQL.Append(" WHERE ");
            bool isFirst = true;
            foreach (var key in keyNames)
            {
                if (false == isFirst)
                {
                    delSQL.Append(" AND ");
                }
                delSQL.Append(key);
                delSQL.Append(" = ? ");
                isFirst = false;
            }

            // トランザクション開始
            var trans = destConn.BeginTransaction();

            // 複製本のデータ読み出し
            int recCnt = 0;
            OdbcCommand selectCmd = new OdbcCommand("SELECT * FROM " + tableName, srceConn);
            var rdr = selectCmd.ExecuteReader();
            while (rdr.Read())
            {
                Console.Write("record {0}\r", recCnt++);

                // フィールド位置検索用にインデックス作成
                if (columnIndexs.Keys.Count == 0)
                {
                    for (int i = 0; i < rdr.FieldCount; i++)
                    {
                        columnIndexs.Add(rdr.GetName(i), i);
                    }
                }

                // INSERTコマンド文字列の生成
                if (insSql == null)
                {
                    insSql = new StringBuilder();
                    insSql.Append(" INSERT INTO ");
                    insSql.Append(tableName);
                    insSql.Append(" ( ");
                    for (int i = 0; i < rdr.FieldCount; i++)
                    {
                        if (i > 0)
                        {
                            insSql.Append(",");
                        }
                        insSql.Append(rdr.GetName(i));
                    }

                    insSql.Append(" ) VALUES ( ");
                    for (int i = 0; i < rdr.FieldCount; i++)
                    {
                        if (i > 0)
                        {
                            insSql.Append(",");
                        }
                        insSql.Append("?");
                    }
                    insSql.Append(" ) ");
                }

                // DELETEコマンドのパラメータ設定と実行
                int keyCnt = 0;
                OdbcCommand delCmd = new OdbcCommand(delSQL.ToString(), destConn, trans);
                foreach (var key in keyNames)
                {
                    delCmd.Parameters.Add(new OdbcParameter("arg" + keyCnt.ToString("00"), rdr.GetValue(columnIndexs[key])));
                }
                delCmd.ExecuteNonQuery();

                // INSERTコマンドのパラメータ設定と実行
                keyCnt = 0;
                OdbcCommand insCmd = new OdbcCommand(insSql.ToString(), destConn, trans);
                for (int i = 0; i < rdr.FieldCount; i++)
                {
                    insCmd.Parameters.Add(new OdbcParameter("arg" + keyCnt.ToString("00"), rdr.GetValue(i)));
                }
                insCmd.ExecuteNonQuery();
            }

            Console.WriteLine("{0} Records Copyed...", recCnt);
            trans.Commit();
        }
    }