Windows上のNode.jsでffiを使ってキーボード入力をする

いわゆるキーボード入力をエミュレートするというものです。

## 準備

[node-gyp](https://github.com/nodejs/node-gyp)が動作する環境を整えておいてください。コンパイル環境は管理者権限のコマンドプロンプトで

```
npm install --global --production windows-build-tools
```

と実行すればできるようになります。[^1]

[^1]: Python 2.7のインストール先は実行ユーザーのプロファイルになります。インストール時の管理者と実際使うユーザーが異なる場合は手動で準備した方が無難でしょう。

必要なnpmパッケージは次の五つです。それぞれ入れてください。

* ffi
* ref
* ref-struct
* ref-union
* ref-array

`ffi`がDLLを直接読み込んで実行します。`ref`および`ref-*`は構造体、共用体、配列などを行うのに必要になります。

## コード

面倒だったので[CoffeeScript](http://coffeescript.org/)で書いています。CoffeeScriptを知らないって人は[Try CoffeeScript](http://coffeescript.org/#try:import%20ref%20from%20'ref'%0Aimport%20ffi%20from%20'ffi'%0Aimport%20StructType%20from%20'ref-struct'%0Aimport%20UnionType%20from%20'ref-union'%0Aimport%20ArrayType%20from%20'ref-array'%0A%0A%23%23%23%0ASendInput%20function%0A%20%20https%3A%2F%2Fmsdn.microsoft.com%2Flibrary%2Fwindows%2Fdesktop%2Fms646310.aspx%0A%23%23%23%0A%0A%23%20check%20pointer%20size%20for%20defined%20ULONG_PTR%0AptrSize%20%3D%20ref.NULL_POINTER.length%0AulongPtr%20%3D%20switch%20ptrSize%0A%20%20when%204%0A%20%20%20%20ref.types.uint32%0A%20%20when%208%0A%20%20%20%20ref.types.uint64%0A%20%20else%0A%20%20%20%20throw%20new%20Error(%22Supported%20pointer%20size%20is%204%20or%208%2C%20but%20is%20%23%7BptrSize%7D%22)%0A%0AMouseInputType%20%3D%20StructType%0A%20%20dx%3A%20ref.types.long%0A%20%20dy%3A%20ref.types.long%0A%20%20mouseData%3A%20ref.types.uint32%0A%20%20dwFlags%3A%20ref.types.uint32%0A%20%20time%3A%20ref.types.uint32%0A%20%20dwExtraInfo%3A%20ulongPtr%0A%0AKeybdInputType%20%3D%20StructType%0A%20%20wVk%3A%20ref.types.uint16%0A%20%20wScan%3A%20ref.types.uint16%0A%20%20dwFlags%3A%20ref.types.uint32%0A%20%20time%3A%20ref.types.uint32%0A%20%20dwExtraInfo%3A%20ulongPtr%0A%0AHardwareInputType%20%3D%20StructType%0A%20%20uMsg%3A%20ref.types.uint32%0A%20%20wParamL%3A%20ref.types.uint16%0A%20%20wParamH%3A%20ref.types.uint16%0A%0AUnionInputType%20%3D%20UnionType%0A%20%20mi%3A%20MouseInputType%0A%20%20ki%3A%20KeybdInputType%0A%20%20hi%3A%20HardwareInputType%0A%0AInputType%20%3D%20StructType%0A%20%20type%3A%20ref.types.uint32%0A%20%20u%3A%20UnionInputType%0A%0AInputArrayType%20%3D%20ArrayType(InputType)%0A%0Auser32%20%3D%20ffi.Library%20'user32'%2C%0A%20%20SendInput%3A%20%5Bref.types.uint%2C%20%5Bref.types.uint%2C%20InputArrayType%2C%20ref.types.int%5D%5D%0A%0A%23%20Virtual-Key%20Codes%0A%23%20%20%20https%3A%2F%2Fmsdn.microsoft.com%2Flibrary%2Fwindows%2Fdesktop%2Fdd375731.aspx%0Akeys%20%3D%20%5B0x48%2C%200x4F%2C%200x47%2C%200x45%5D%20%23%20hoge%0Ainputs%20%3D%20new%20InputArrayType(keys.length)%0A%0Afor%20vk%2C%20i%20in%20keys%0A%20%20inputs%5Bi%5D.type%20%3D%201%20%23%20INPUT_KEYBOARD%0A%20%20inputs%5Bi%5D.u.ki.wVk%20%3D%20vk%0A%20%20inputs%5Bi%5D.u.ki.wScan%20%3D%200%0A%20%20inputs%5Bi%5D.u.ki.dwFlags%20%3D%200%0A%20%20inputs%5Bi%5D.u.ki.time%20%3D%200%0A%20%20inputs%5Bi%5D.u.ki.dw%20%3D%200%0A%0AsetInterval%20-%3E%0A%20%20result%20%3D%20user32.SendInput(inputs.length%2C%20inputs%2C%20InputType.size)%0A%20%20console.log(%22send%3A%20%23%7Bresult%7D%22)%0A%2C%201000%0A)で変換されたコードを見てください。[^2]

[^2]: だれか、ES2015+で整形して書き直したのに置き換えてくれないかな。

```winkey_send.coffee
import ref from 'ref'
import ffi from 'ffi'
import StructType from 'ref-struct'
import UnionType from 'ref-union'
import ArrayType from 'ref-array'

###
SendInput function
  https://msdn.microsoft.com/library/windows/desktop/ms646310.aspx
###

# check pointer size for defined ULONG_PTR
ptrSize = ref.NULL_POINTER.length
ulongPtr = switch ptrSize
  when 4
    ref.types.uint32
  when 8
    ref.types.uint64
  else
    throw new Error("Supported pointer size is 4 or 8, but is #{ptrSize}")

MouseInputType = StructType
  dx: ref.types.long
  dy: ref.types.long
  mouseData: ref.types.uint32
  dwFlags: ref.types.uint32
  time: ref.types.uint32
  dwExtraInfo: ulongPtr

KeybdInputType = StructType
  wVk: ref.types.uint16
  wScan: ref.types.uint16
  dwFlags: ref.types.uint32
  time: ref.types.uint32
  dwExtraInfo: ulongPtr

HardwareInputType = StructType
  uMsg: ref.types.uint32
  wParamL: ref.types.uint16
  wParamH: ref.types.uint16

UnionInputType = UnionType
  mi: MouseInputType
  ki: KeybdInputType
  hi: HardwareInputType

InputType = StructType
  type: ref.types.uint32
  u: UnionInputType

InputArrayType = ArrayType(InputType)

user32 = ffi.Library 'user32',
  SendInput: [ref.types.uint, [ref.types.uint, InputArrayType, ref.types.int]]

# Virtual-Key Codes
#   https://msdn.microsoft.com/library/windows/desktop/dd375731.aspx
keys = [0x48, 0x4F, 0x47, 0x45] # hoge
inputs = new InputArrayType(keys.length)

for vk, i in keys
  inputs[i].type = 1 # INPUT_KEYBOARD
  inputs[i].u.ki.wVk = vk
  inputs[i].u.ki.wScan = 0
  inputs[i].u.ki.dwFlags = 0
  inputs[i].u.ki.time = 0
  inputs[i].u.ki.dw = 0

setInterval ->
  result = user32.SendInput(inputs.length, inputs, InputType.size)
  console.log("send: #{result}")
, 1000
```

## 解説

[SendInput関数](https://msdn.microsoft.com/library/windows/desktop/ms646310.aspx)を使用しています。Cのコードを`ffi`を用いて実現しているだけに過ぎません。

マウスもできると思いますが、試していません。他にもあまりテストしていませんので、うまくいかなかったら、コメント等残してくれると助かります。特に、32bit環境は試す環境が無いので、うまくいくかはかなり不安です。

終了はCtrl+Cなどで強制終了してください。

構造体、共用体、配列が組み合わさった場合に`ffi`にどう読み込ませるかのサンプルが全然見当たらなかったので、本当にこれでいいのかはちょっと不安です。一応、上のコードは手元では動いてはいるのですが。

## 参考

[Node.jsでWindowsのキー状態を取得する](https://qiita.com/niusounds/items/54a336fd5b9fadc85ef1)
目的は逆ですが、`ffi`の使い方の参考にさせていただきました。