RustでWindowsサービス作成!windows-rs利用

開発
Krzysztof PlutaによるPixabayからの画像

※この記事にはプロモーションが含まれています。

存在感が日々高まっているRustで、これまでC/C++で開発してきた領域の置き換えを徐々に実行していきたいと考えています。今回は堅牢実装が用途にピッタリ当てはまる、windows-rsクレートを用いたWindowsサービスモジュール作成手順を紹介します。

スポンサーリンク
スポンサーリンク

はじめに

サービスモジュール類は常に動作しかつ重要なロジックが多いので、特に脆弱性やメモリ管理に気を配る必要があります。サービス開発にRustを利用することでその恩恵を受けるために、Windowsサービスの大枠を実装してみたいと思います。Windows APIの実装を容易にするため、マイクロソフトが開発を進めている、win32metadataプロジェクトからwindows-rsを利用します。

windows-rsサイトでの開発手法を参考にしながら、全体の進め方Windows API関連の構成を確認して、開発を進めていきます。

RustでWindows API使用!懐かしいウィンドウ作成の記事で、簡単なWindowsモジュール開発も紹介しているので、参考にしてください。

プロジェクトのひな型作成

コマンドプロンプトから、下記コマンドを実行してプロジェクトのひな型を作成します。libを追加しているのは、こちらにwindows-rsのモジュールを登録しておくと、毎回のビルド時間をキャッシュにより短縮できるためです。

ccargo new test_service

cd test_service

cargo new --lib bindigns

コード記載

各ファイルにソースコードを記載して、実装していきます。

bindings/Cargo.tomlファイル

利用するクレートに”windows“を登録します。バージョンは適宜変更してください。

[dependencies]
windows = "0.9.1"

[build-dependencies]
windows = "0.9.1"

bindings/src/lib.rsファイル

元々記述されているコードを全て削除し、下記1行に書き換えます。

::windows::include_bindings!();

bindings/build.rsファイル

bindings配下にbuild.rsファイルを新規に作成して追加します。

このファイルには、使用するwindows-rsのAPIおよび構造体を追加していきます。名前空間は、前述のAPIサイトからAPI名で検索します。文字列を扱うAPIにはA系とW系の2つが登録されているので注意です。

fn main() {
    windows::build!(
        Windows::Win32::Security::{
            StartServiceCtrlDispatcherW, RegisterServiceCtrlHandlerExW, SetServiceStatus, SERVICE_ACCEPT_STOP, SERVICE_ACCEPT_PAUSE_CONTINUE
        },
        Windows::Win32::WindowsProgramming::{ CloseHandle, INFINITE },
        Windows::Win32::SystemServices::{ CreateEventW, SetEvent, WaitForMultipleObjects },
        Windows::Win32::Debug::{ OutputDebugStringW, WIN32_ERROR },
    );
}

src/Cargo.tomlファイル

必要な関連モジュールを登録しています。

[dependencies]
windows = "0.9.1"
bindings = { path = "bindings" }
once_cell = "1.7.2"

src/main.rsファイル

以降は、全てmain.rsファイルに追記していきます。

まずは、必要なクレート群を呼び出します。

use bindings::{
    Windows::Win32::Debug::{ OutputDebugStringW, WIN32_ERROR }, 
    Windows::Win32::Security::{ StartServiceCtrlDispatcherW, RegisterServiceCtrlHandlerExW, SetServiceStatus, SERVICE_TABLE_ENTRYW, 
        SERVICE_STATUS, ENUM_SERVICE_TYPE, SERVICE_STATUS_HANDLE, SERVICE_STATUS_CURRENT_STATE, SERVICE_ACCEPT_STOP, SERVICE_ACCEPT_PAUSE_CONTINUE }, 
    Windows::Win32::SystemServices::{ CreateEventW, SetEvent, WaitForMultipleObjects, PWSTR, HANDLE }, 
    Windows::Win32::WindowsProgramming::{ CloseHandle, INFINITE }
};
use std::{ffi::c_void};
use once_cell::sync::OnceCell;

関数を跨いで使用するグローバル変数を定義します。現状は、サービス名・イベントハンドル・サービスハンドルです。

const SERVICE_NAME: &str = "NewService";
static EVENT_STOP_SERVICE: OnceCell<HANDLE> = OnceCell::new();
static EVENT_DUMMY_ACTION: OnceCell<HANDLE> = OnceCell::new();
static HANDLE_SERVICE_STATUS: OnceCell<SERVICE_STATUS_HANDLE> = OnceCell::new();

何度か使用するユーティリティ的関数を用意しました。

  • UTF-8文字列をUTF-16に変換する
  • 動作状況をデバッガに表示する
fn encode(source: &str) -> Vec<u16> {
    source.encode_utf16().chain(Some(0)).collect()
}

fn my_debug_string(text: &str){
    let s = PWSTR(encode(text).as_mut_ptr());
    unsafe {
        OutputDebugStringW(s);
    }
}

サービスマネージャーにサービスのステータスを通知する、関数を作成しています。

pub unsafe extern "system" fn set_service_status(current_status: SERVICE_STATUS_CURRENT_STATE, check_point: u32, wait_hint: u32, control_accepted: u32){
    let mut status = SERVICE_STATUS {
        dwServiceType: ENUM_SERVICE_TYPE::SERVICE_WIN32_OWN_PROCESS,
        dwCurrentState: current_status,
        dwControlsAccepted: control_accepted,
        dwWin32ExitCode: 0,
        dwServiceSpecificExitCode: 0,
        dwCheckPoint: check_point,
        dwWaitHint: wait_hint
    };

    SetServiceStatus(*HANDLE_SERVICE_STATUS.get().unwrap(), &mut status);
}

サービスのコントロール要求を処理するハンドラ関数です。最低限のSERVICE_CONTROL_STOPだけ処理しています。処理詳細は、LPHANDLER_FUNCTION_EX callback functionを参照してください。

pub unsafe extern "system" fn handler_ex(dwcontrol: u32, _dweventtype: u32, _lpeventdata: *mut c_void, _lpcontext: *mut c_void) -> u32 {
    let mut ret: u32 = WIN32_ERROR::NO_ERROR.0;

    match dwcontrol {
        SERVICE_CONTROL_STOP => {
            my_debug_string("SERVICE_CONTROL_STOP\n");
            SetEvent(*EVENT_STOP_SERVICE.get().unwrap());
        },
        SERVICE_CONTROL_CONTINUE => {
            my_debug_string("SERVICE_CONTROL_CONTINUE\n");
            ret = WIN32_ERROR::ERROR_CALL_NOT_IMPLEMENTED.0;
        },
        SERVICE_CONTROL_PAUSE => {
            my_debug_string("SERVICE_CONTROL_PAUSE\n");
            ret = WIN32_ERROR::ERROR_CALL_NOT_IMPLEMENTED.0;
        },
        _ => {
            // any control codes.
        }
    }

    ret
}

サービスのメイン関数です。コントロールハンドラの登録、サービスマネージャーへのステータス通知、イベントの要求待ち、最後にリソース開放処理を行っています。

イベント要求は、現状サービス停止のみ処理しています。 Rustのシグナル機構で待機することも考えましたが、本モジュールが子ウィンドウを持つ場合ウィンドウメッセージ処理向けにMsgWaitForMultipleObjects APIを使いたくなるので、今はWin32レベルで簡易に処理しています。

pub unsafe extern "system" fn service_main(_dwnumservicesargs: u32, _lpserviceargvectors: *mut PWSTR) {
    let service_name = encode(SERVICE_NAME).as_mut_ptr();
    let handle_service_status = RegisterServiceCtrlHandlerExW(PWSTR(service_name), Some(handler_ex), std::ptr::null_mut());

    HANDLE_SERVICE_STATUS.set(handle_service_status).unwrap();

    set_service_status(SERVICE_STATUS_CURRENT_STATE::SERVICE_START_PENDING, 1, 5000, 0);

    // Create events.
    EVENT_STOP_SERVICE.set(CreateEventW(std::ptr::null_mut(), false, false, None)).unwrap();
    EVENT_DUMMY_ACTION.set(CreateEventW(std::ptr::null_mut(), true, false, None)).unwrap();
    let events = [
        *EVENT_STOP_SERVICE.get().unwrap(), // WAIT_OBJECT_0 + 0
        *EVENT_DUMMY_ACTION.get().unwrap(), // WAIT_OBJECT_0 + 1
    ];

    set_service_status(SERVICE_STATUS_CURRENT_STATE::SERVICE_RUNNING, 0, 0, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE);

    let mut is_running: bool = true;
    while is_running {

        let object_wait = WaitForMultipleObjects(events.len() as u32, events.as_ptr(), false, INFINITE);
        match object_wait {
            WAIT_OBJECT_0 => {
                is_running = false;
            },
            WAIT_OBJECT_1 => {
                // not implement for EVENT_DUMMY_ACTION.
            },
            WAIT_FAILED => {
                is_running = false;
            },
            _ => {
                // do nothing.
            }
        }
    }

    // set service status for SERVICE_STOP_PENDING.
    set_service_status(SERVICE_STATUS_CURRENT_STATE::SERVICE_STOP_PENDING, 1, 2000, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE);
    
    // close event handles.
    for x in events.iter(){
        CloseHandle(x);
    }

    // set service status for SERVICE_STOPPED.
    set_service_status(SERVICE_STATUS_CURRENT_STATE::SERVICE_STOPPED, 0, 0, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE);
}

モジュールのスタート関数です。サービスとしてディスパッチするところから、処理が始まります。

fn main() -> windows::Result<()> {
    
    unsafe {
        let service_name = encode(SERVICE_NAME).as_mut_ptr();
        let svc_table  = SERVICE_TABLE_ENTRYW{lpServiceName: PWSTR(service_name), lpServiceProc: Some(service_main) };

        if StartServiceCtrlDispatcherW(&svc_table) == false {

            my_debug_string("error StartServiceCtrlDispatcherW\n");
        }
    }

    Ok(())
}

サービス実行

サービスの登録・削除

サービスモジュールを、”NewService“と言うサービス名をつけて、昇格したコマンドプロンプトから以下のコマンドでOSに登録します。

sc.exe create NewService binpath= c:\test_service.exe type= own start= auto

サービスが不要もしくは再度やり直し等の処理をしたい場合、以下のコマンドでOSからエントリーを削除してください。

sc.exe delete NewService

サービスの実行

作成したサービスモジュールを、コントロールパネルの”サービス一覧”もしくはscコマンドで実行してみます。

ステータスが“実行中”となって、サービスモジュールが常駐していることが分かります。停止させることも可能です。

まとめ

Windowsサービスモジュールのひな型をRustで実装する手順を紹介しました。サービスモジュールとして常駐できれば、あとはビジネスロジックを追加するだけです。これらのロジックはクリティカルになる場面が多いので、C++らに比べてRustの特性が生きてくると思われます。新しくモジュールを開発する際には、是非Rustの採用を検討してみてください。

以上、RustでWindowsサービス作成の紹介でした。

コメント