The Man Who Fell From The Wrong Side Of The Sky:2019年5月30日分

2019/5/30(Thu)

PowerShellで生活するために - Add-Typeコマンドレット編(その1)

前回の続き、どんどん話が脱線していくけどしょせんは自分用のチラシの裏なのでどうでもいい、なんせPowerShellからWindows APIを呼ばないとセキュリティの基本も満たせないようなクソザコナメクジ言語が悪いよー。

@ ネイティブコードを呼びだす(Java JNI編)

なんでPowerShellとは何の関係も無いJavaの話なんですか!まぁシェフの気まぐれサラダ並みに何も考えてない備忘録がてら。なんせチラシの裏だからな。

ネイティブコード呼ぶにはJNI(Java Native Interface)という規則に従って醜悪なC/C++によるブリッジコードを書く羽目になるのだけど、Java屋にC/C++書かせたらそりゃそのラッパー部分ですらメモリリーク起こすしスレッドアンセーフなコード書いてJVMも毎日クラッシュしますがな *1

まぁJavaは前述の通りJava6以降ならjava.nio.file.Files::createDirectoryがjava.nio.file.FileAlreadyExistsException投げるので、今回やりたいこと的にはネイティブコード書く必要も無いんだけど、いまだにJava1.4以降の機能はすべて禁止の縛りプレイやっとる地獄ありそうだしな(しろめ)。

まずJava側ではnative修飾子を使ってメソッド定義だけ書く、NewTemporaryDirectoryとでもしておこう。

package unko;

import java.io.IOException;

public class Unko {
	static {
		System.loadLibrary("unko");
	}
	public native static String NewTemporaryDirectory() throws IOException;
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

System.loadLibraryの引数がDLLなんかの共有ライブラリ名、Windowsならunko.dllが検索されてロードされる。

そんでお次はJava9でjavahコマンドはdeprecatedとなり削除されとったので、javacコマンドで

$ javac -h ヘッダ出力先 Unko.java

としてヘッダファイルを出力する、するとこんなのが出力される。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class unko_Unko */

#ifndef _Included_unko_Unko
#define _Included_unko_Unko
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     unko_Unko
 * Method:    NewTemporaryDirectory
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_unko_Unko_NewTemporaryDirectory
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

あとはこいつの実体を実装し、unko.dllをビルドしてやればおk。

#include <Windows.h>
#include <combaseapi.h>
#pragma comment(lib, "ole32.lib")
#include <strsafe.h>

#include "unko_Unko.h"

#define arraycount(array)       (sizeof((array))/sizeof((array)[0]))

JNIEXPORT jstring JNICALL
Java_unko_Unko_NewTemporaryDirectory(JNIEnv* env, jclass class)
{
	SECURITY_ATTRIBUTES sa;
	DWORD tmplen;
	WCHAR tmp[MAX_PATH], path[MAX_PATH];
	GUID guid;
	LPOLESTR guidstr;

	/* XXX: null security descriptor means default security, so this is NOT SAFE. */
	sa.nLength = sizeof(sa);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle = TRUE;
	tmplen = GetTempPathW(arraycount(tmp), tmp);
	if (StringCchCopyW(path, arraycount(path), tmp) == S_OK) {
		for (;;) {
			if (CoCreateGuid(&guid) != S_OK)
				break;
			if (StringFromCLSID(&guid, &guidstr) != S_OK)
				break;
			if (StringCchCopyW(path + tmplen, arraycount(path) - tmplen, (LPCWSTR)guidstr) != S_OK) {
				CoTaskMemFree(guidstr);
				break;
			}
			CoTaskMemFree(guidstr);
			if (CreateDirectoryW(path, &sa))
				return (*env)->NewString(env, (const jchar*)path, (jsize)lstrlenW(path));
			if (GetLastError() != ERROR_ALREADY_EXISTS)
				break;
		}
	}
	(*env)->ThrowNew(env, (*env)->FindClass(env, "java/io/IOException"), "something wrong.");
	return NULL;
}

アクセス権周りのコードまで例に入れると長いので省略よってデフォルトのアクセス権がつくことに注意、なので作業用ディレクトリとして使うには安全でない。 PowerShellで書いてすらあの長さなのでWindows API使ってC/C++で書いたら地獄となる典型的なコードだからな…

@ ネイティブコードを呼びだす(Java JNA編)

さすがにJNIによる被害多数により、ブリッジコード不要なJNA(Java Native Access)という規格が作られ、現在では以下のコードのようにすべてJavaで書くことも可能となっている *2

package unko;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.UUID;

import com.sun.jna.IntegerType;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.win32.W32APIOptions;

public class Unko {
	interface WinError {
		static final int ERROR_ALREADY_EXISTS = 183;
	}
	public static class DWORD extends IntegerType {
		public DWORD() {
			this(0);
		}
		public DWORD(long value) {
			super(4, value, true);
		}
	}
	public static class BOOL extends IntegerType {
		public BOOL() {
			this(false);
		}
		public BOOL(boolean value) {
			super(4, value ? 1 : 0, true);
		}
	}
	@FieldOrder({"nLength", "lpSecurityDescriptor", "bInheritHandle"})
	public static class SECURITY_ATTRIBUTES extends Structure {
		public DWORD nLength;
		public Pointer lpSecurityDescriptor;
		public BOOL bInheritHandle;
	}
	public interface Kernel32 extends Library {
		Kernel32 INSTANCE = (Kernel32)Native.load("kernel32", Kernel32.class, W32APIOptions.DEFAULT_OPTIONS);
		boolean CreateDirectory(String lpPathName, SECURITY_ATTRIBUTES lpSecurityAttributes);
		int GetLastError();
	}
	public static String NewTemporaryDirectory() throws IOException {
		/* XXX: null security descriptor means default security, so this is NOT SAFE. */
		SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
		sa.nLength = new DWORD((long)sa.size());
		sa.lpSecurityDescriptor = null;
		sa.bInheritHandle = new BOOL(true);
		String tmp = System.getProperty("java.io.tmpdir");
		for (;;) {
			String path = Paths.get(tmp, UUID.randomUUID().toString()).toString();
			if (Kernel32.INSTANCE.CreateDirectory(path, sa))
				return path;
			if (Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS)
				break;
		}
		throw new IOException("something wrong.");
	}
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

長げえよクソが、ただでさえドイヒーな設計なWindows APIに大量に存在する独自な型定義をいちいちJavaでの定義に再翻訳する必要あって二重苦もいいとこですわやっぱりちゃんとC/C++書けるプログラマ連れてきた方が楽なんじゃねえの(テノヒラクルー)。

それにNobody RunsならぬRun Anywareが信条のJavaだし、直接システムのネイティブコードを呼ぶのではなくブリッジコードでOSの違いを吸収しとけってのも正しいんだけどね…

話を戻して、Run Anywhereを捨てて直接システムのネイティブコード叩くなら、Windows APIのようなWell Knownなものは contribにぜんぶ定義済のコードがあるので、わざわざ自分で書かずにjarつっこめば再翻訳の手間だけは省ける(楽になるとはいっていない)。

package unko;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.UUID;

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinError;

class Unko {
	public static String NewTemporaryDirectory() throws IOException {
		/* XXX: null security descriptor means default security, so this is NOT SAFE. */
		WinBase.SECURITY_ATTRIBUTES sa = new WinBase.SECURITY_ATTRIBUTES();
		sa.lpSecurityDescriptor = null;
		sa.bInheritHandle = true;
		String tmp = System.getProperty("java.io.tmpdir");
		for (;;) {
			String path = Paths.get(tmp, UUID.randomUUID().toString()).toString();
			if (Kernel32.INSTANCE.CreateDirectory(path, sa))
				return path;
			if (Kernel32.INSTANCE.GetLastError() != WinError.ERROR_ALREADY_EXISTS)
				break;
		}
		throw new IOException("something wrong.");
	}
	public static void main(String[] argv) throws IOException {
		System.out.println(NewTemporaryDirectory());
	}
}

@ ネイティブコードを呼びだす(C# P/Invoke編)

一方C#などの.Net Frameworkの場合、最初からこのJNAと似た「P/Invoke(Platform Invoke)」という仕組が用意されている。 なお「ピ○ボケ」と読んでしまうと放送禁止用語に抵触するので以下略、電気羊のイジドアかな?

そういえばC#っていちども仕事で使ったこと無いんだよね、なのでどういう言語かは

Borlandのリストラを逃れたHejlsbergとJ++を待っていたのはまた地獄だった
Microsoftを訴えたのはInpriseと名を変えた古巣とJavaのSun Microsystem、OSとRADの百年戦争が生み出した法廷闘争
J++とDelphi、Windows APIとOLE/COM/ActiveXをコンクリートミキサーにかけてぶちまけた
それがVisual Studioの最新リリース

次回「.NET」

来週もHejlsbergと地獄に付き合ってもらう

という認識でよかったんだっけ…まってC#作ったのになんでJ#作ったんだ…?

そいやかつて彷徨ってた見知らぬ街の住人たち、いまだに後生大事にVisual Basic 6.0を使い続けているのだろうか、まぁWindows 10 32bitでRuntimeサポートされ続けるそうだし使い続けるんだろうな…むせる。

んでC#でのコード例はとりあえずこんな感じ、アクセス権周りについてはやっぱり省略。

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace unko {
	class Unko {
		internal const int ERROR_ALREADY_EXISTS = 183;
		[StructLayout(LayoutKind.Sequential)]
		internal struct SECURITY_ATTRIBUTES {
			internal uint nLength;
			internal IntPtr lpSecurityDescriptor;
			internal bool bInheritHandle;
		}
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes);
		static String NewTemporaryDirectory() {
			/* XXX: null security descriptor means default security, so this is NOT SAFE. */
			SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
			sa.nLength = (uint)Marshal.SizeOf(sa);
			sa.lpSecurityDescriptor = IntPtr.Zero;
			sa.bInheritHandle = true;
			String tmpdir = Path.GetTempPath();
			for (;;) {
				String path = tmpdir + Path.GetRandomFileName();
				if (CreateDirectory(path, ref sa))
					return path;
				if (Marshal.GetLastWin32Error() != ERROR_ALREADY_EXISTS)
					break;
			}
			throw new IOException();
		}
		static void Main(string[] args) {
			Console.WriteLine(NewTemporaryDirectory());
		}
	}
}

いちいち構造体やらを再定義する手間はJNAに似てるけど *3、アトリビュート書くだけでネイティブコードをクラスメソッドにできるのはJNIのnative修飾子まではいかないけど手軽よね、しかもJNIと違ってブリッジコード不要だしJNAのようにpublicにする必要も無い。

Javaもどうでもいいとこにアノテーション乱用して地獄絵図になっとるのにこういうとこで使わないからクソ言語といわれるのだ。まぁちょっとP/Invokeのこのアトリビュート名はWindowsベタ過ぎるきらいはあるけどな…と思ったらMonoのドキュメント読んだら

[DllImport ("libc.so")]
private static extern int getpid();

うーんこの、LinuxでもDllImportなのか…

ところでマーシャルという用語をみるたびに何段にも積まれたギターアンプを想像するんだけど語源一緒なんだな、そもそも人名だったり軍隊とか警察のお偉いさんの意味なのに、なんでコンピューター用語では尻洗いズみたいな意味になっているのか…

@ PowerShellからP/Invokeを使う

ようやく本題のPowerShellのお話、だが残念なことにPowerShellそのものにはP/Invokeの機能は備わっていないのだ。 しかしAdd-Typeコマンドレットというものを使うと、PowerShellスクリプト中にC#のコードが書けてるので、その中でP/Invokeすることで代用することができる。

Add-Typeに渡すC#コードは文字列なので、読みやすいようヒアドキュメント風に書くと読みやすい。

Add-Type -Language 'CSharp' -TypeDefinition @'
namespace unko {
	class Unko {
		...
		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
		internal static extern bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes);
		static String NewTemporaryDirectory() {
			...
		}
	}
}
'@
[unko.Unko]::NewTemporaryDirectory()

そういえば20年経ってもヒアドキュメント実装されてなくて 未だに提案止まりのクソ言語がありましたね…

ちなみに名前空間やクラス名はオプションでも指定可能、その場合は-TypeDefinitionでなく-MemberDefinitionを使う

Add-Type -Language 'CSharp' -Namespace 'unko' -Name 'Unko' -MemberDefinition @'
static String NewTemporaryDirectory() {
	...
}
'@
[unko.Unko]::NewTemporaryDirectory()

たいして省力化できないし判りづらいよなこれ。

まぁともかくバックグラウンドで

  • -TypeDefinition/-MemberDefinitionの引数の文字列が一時ファイルに書きだされ
  • -Languageで指定した言語に対応するコンパイラが一時ファイルをコンパイル
  • ビルドされたライブラリ(アセンブリ)をPowerShellのAppDomainに動的ロードされる

という感じ。

タッパーウェアシェルスクリプトでもCソースをヒアドキュメントで書いて、それを標準入力でコンパイラに食わせて実行ファイル作るみたいな苦し紛れ考えることはあるでしょ。

#!/bin/sh
f=/var/tmp/unko
gcc -xc - -o $f >/dev/null << EOF;
#include <paths.h>
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
	char buf[PATH_MAX];
	char *path;
	strlcpy(buf, _PATH_TMP, sizeof(buf));
	strlcat(buf, "unko.XXXXXX", sizeof(buf));
	path = mkdtemp(buf);
	if (path == NULL)
		exit(EXIT_FAILURE);
	puts(path);
	exit(EXIT_SUCCESS);
}
EOF
tmpdir=`$f`
rm -f $f
...

ところであなたがお使いの開発マシンだけでなく、実運用環境にもコンパイラがあるってちゃんと確認しましたか(小声)。

PowerShellの場合はとくに.Net Framework SDKやVisual Studioなどがインストールされてる必要は無くランタイムだけでいいようなので、今回みたいに使わざるをえない状況なら使ってけという感じ。

ただし注意点はAdd-TypeはできてもRemove-Typeが存在しないので(AppDomainの仕様らしい)ちょこっと書換えて再実行とかすると

Add-Type : Cannot add type. The type name 'unko.Unko' already exists.
At line:1 char:1
+ Add-Type -Language 'CSharp' -Namespace 'unko' -Name 'Unko' -MemberDef ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (unko.Unko:String) [Add-Type], Exception
    + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand
 

とエラーが出て、古いコードが実行されて事故起こす可能性があるのがなんともアレ。-ErrorAction Stopとかしても例外は飛ばないようだしどうやって回避したもんですかね…

@ 次回

完全に飽きてるんだけど、残りのコード書いて完成させるよ…

*1:某所でWindowsから某商用UNIX向けに移植されたライブラリがグローバル変数だらけでJNIでクラッシュ以下略、機密漏洩とか大事故になるくらい動作してたら恐ろしいとこだったって通りすがりのイルカがいってた。
*2:とはいえJNAもJNIベースの技術なので最低限のブリッジコードはあるんだけどね、libffiベースで書かれててFreeBSDやOpenBSD用のバイナリもリリースに含まれている、なおNなんとかBSD(限界集落)
*3:こっちももしかするとJNAのcontribみたいに定義済のものあるのかもしれない、内部的にはCoreLibのInteropを使ってるみたいだけど公開されてるんかなこれ。