はじめに
この記事はできなかった系記事
です。この記事を参考にしても何も生まないので成功例を知りたい方は自身でやってください。
大事な事なのでもう一回書きます。
本記事に書かれてるコードを参考にしても何も生みません。
尚、こんな感じならできるんじゃないの?
みたいな意見があれば教えてもらえると私うれしいです。
動機(供述)
Rust使えるって言いたいし、なんかいい題材探してたらVNCppというすごいプロダクトがあった。
リモートワークはやってるしVNC使えたら俺の株も上がるハズと思った。
結果
できんかった(画像付き)
やったこと
下記手順で淡々とアプリを作成(完成しない)してゆく。
- 構想を練る
- 開発環境構築
- rfbClientをラップ
- Android側IFを定義
- 間を作る <- ここで断念
構想を練る
VNCppはrfbClientを扱うアプリなので、rfbClientをラップして、いい感じに呼び出す。<- 後々これが厳しくなる
開発環境構築
GitBashにて開発する。MozillaからBuilding and Deploying a Rust library on Androidという素晴らしい資料が提供されているので、一礼してマネする。
Windowsの人は~/.cargo/config
の指定で悩むかもしれないので一応設定を残しておく。
[target.aarch64-linux-android]
ar = "c:\\path\\to\\aarch64-linux-android-ar"
linker = "c:\\path\\to\\aarch64-linux-android-clang.cmd"
[target.armv7-linux-androideabi]
ar = "c:\\path\\to\\arm-linux-androideabi-ar"
linker = "c:\\path\\to\\arm-linux-androideabi-clang.cmd"
[target.i686-linux-android]
ar = "c:\\path\\to\\i686-linux-android-ar"
linker = "c:\\path\\to\\i686-linux-android-clang.cmd"
マネし終わったら、cppを抱きかかえてビルドするための設定を行う。
Cargo.tomlはこんな感じ。
[package]
name = "vncrust"
version = "0.1.0"
authors = ["ハズイので隠す"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = "0.2.12"
lazy_static = "1.4.0"
[build-dependencies]
cc = { version = "1.0.50", features = ["parallel"] }
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }
libc = "0.2.67"
[lib]
crate-type = ["dylib"]
name = "vncrust"
build.rsはこんな感じ。rfb_client_wrapper.cppに関しては後述。
extern crate cc;
fn main() {
cc::Build::new()
.cpp(true)
.warnings(true)
.flag("-Wall")
.file("./src/cpp/rfb_client_wrapper.cpp")
.include("./src/cpp/include")
.shared_flag(true)
.static_flag(true)
.compile("rfb")
}
rfbのinclude
mkdir -p ./src/cpp/include
cp -r path/to/vncpp/src/vncpp/jni/include/* ./src/cpp/include/
尚、これだけだとビルドに失敗するので、ビルドするタイミングでCCなどの環境変数を設定する。
export CC="c:\\path\\to\\aarch64-linux-android-clang.cmd"
export CXX="c:\\path\\to\\aarch64-linux-android-clang.cmd"
export AR="c:\\path\\to\\aarch64-linux-android-ar"
cargo build --target aarch64-linux-android --releasee
rfbClientをラップ
VNCppをざっくりいうと、libvncclientのrfbClientを利用するアプリだ。と言うわけで、rfbClientのメソッドをラッピングしてゆく。と言っても、解析しながら必要な関数を適宜追加していったような感じ。
src/cpp/include/rfb_client_wrapper.h
#include <rfb/rfbclient.h>
src/cpp/rfb_client_wrapper.cpp
#include "rfb_client_wrapper.h"
#define bitsPerSample 8
#define samplesPerPixel 3
#define bytesPerPixel 4
// need typedef function pointer
extern "C" {
typedef char* (*GET_PASS)(_rfbClient*);
typedef signed char (*INI_FRAME_BUFFER)(_rfbClient*);
typedef void (*UPDATE_SCREEN)(_rfbClient*, int, int, int, int);
typedef void (*FINISH_UPDATE)(_rfbClient*);
typedef struct {
rfbClient impl;
} rfbClientImpl;
rfbClientImpl* rfbClient_rfbClient() {
rfbClient* client = rfbGetClient(bitsPerSample, samplesPerPixel, bytesPerPixel);
return (rfbClientImpl*)client;
}
/*
* setter
*/
void rfbClient_setServerPort(rfbClientImpl* client, int port){
client->impl.serverPort = port;
}
void rfbClient_setServerHost(rfbClientImpl* client, char* host){
client->impl.serverHost = host;
}
void rfbClient_setProgramName(rfbClientImpl* client){
client->impl.programName = "VNCrust";
}
void rfbClient_setAppData_QualityLevel(rfbClientImpl* client, int picture_quality){
client->impl.appData.qualityLevel = picture_quality;
}
void rfbClient_setAppData_CompressLevel(rfbClientImpl* client, int compress){
client->impl.appData.compressLevel = compress;
}
void rfbClient_setAppData_UseRemoteCursor(rfbClientImpl* client, bool hide_mouse){
client->impl.appData.useRemoteCursor = hide_mouse;
}
void rfbClient_setCanHandleNewFBSize(rfbClientImpl* client){
client->impl.canHandleNewFBSize = TRUE;
}
void rfbClient_setListenPort(rfbClientImpl* client){
client->impl.listenPort = LISTEN_PORT_OFFSET;
}
void rfbClient_setListen6Port(rfbClientImpl* client){
client->impl.listen6Port = LISTEN_PORT_OFFSET;
}
/*
* getter
*/
unsigned char* rfbClient_getFrameBuffer(rfbClientImpl* client){
return client->impl.frameBuffer;
}
/*
* handler
*/
void rfbClient_setGetPassword(rfbClientImpl* client, GET_PASS getPass){
client->impl.GetPassword = getPass;
}
void rfbClient_setMallocFrameBuffer(rfbClientImpl* client, INI_FRAME_BUFFER iniFrameBuffer){
client->impl.MallocFrameBuffer = iniFrameBuffer;
}
void rfbClient_setGotFrameBufferUpdate(rfbClientImpl* client, UPDATE_SCREEN updateScreen){
client->impl.GotFrameBufferUpdate = updateScreen;
}
void rfbClient_setFinishedFrameBufferUpdate(rfbClientImpl* client, FINISH_UPDATE finishUpdate){
client->impl.FinishedFrameBufferUpdate = finishUpdate;
}
/*
* caller
*/
void rfbClient_close(rfbClientImpl* client) {
close(client->impl.sock);
}
void rfbClient_free(rfbClientImpl* client) {
free(client->impl.frameBuffer);
}
int rfbClient_WaitForMessage(rfbClientImpl* client,unsigned int usecs) {
return WaitForMessage(&client->impl, usecs);
}
rfbBool rfbClient_HandleRFBServerMessage(rfbClientImpl* client) {
return HandleRFBServerMessage(&client->impl);
}
}
Android側IFを定義
lib.rsに定義する。ざっくり説明すると、
- チャンネル作って
- iniJniでスレッド作って
- iniConnectでRfbClient作って
- 各要求があればそれに応じた処理をする
ってやつ。字面からすると簡単そう。(字面からすると)
lib.rs
#[macro_use] extern crate lazy_static;
#[cfg(target_os="android")]
pub mod rfb_client;
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
use self::jni::JNIEnv;
use self::jni::objects::{JClass, JString};
use self::jni::sys::{jint, jboolean};
use std::ffi::{CStr, CString};
use std::thread;
use std::sync::{Mutex, Arc, mpsc};
use std::sync::mpsc::{Sender, Receiver};
use std::time::Duration;
use crate::rfb_client::{RfbClient, RfbClientImpl};
lazy_static! {
static ref COMMAND_CHANNEL: JniCommandChannel = {
let (tx, rx): (Sender<JniCommand>, Receiver<JniCommand>) = mpsc::channel();
JniCommandChannel {
tx: Mutex::new(tx),
rx: Mutex::new(rx),
}
};
static ref VNC_PASSWORD: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
}
/*
* JNI Export
*/
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_iniConnect( env: JNIEnv, _: JClass,
host_java: JString, port_java: jint ,pass_java: JString,
quality_java: jint,conpress_java: jint ,hide_mouse: jboolean ) -> jint{
let port = port_java;
let quality = quality_java;
let compress = conpress_java;
let aux_host = get_string(&env, host_java, String::from("localhost"));
let aux_pass = get_string(&env, pass_java, String::from(""));
let vnc_password = Arc::clone(&VNC_PASSWORD);
let mut password = vnc_password.lock().unwrap();
*password = aux_pass.clone();
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::IniConnect,
port: port,
quality: quality,
compress: compress,
host: aux_host,
pass: aux_pass,
hide_mouse: match hide_mouse {
0 => true,
_ => false,
},
mouse_x: 0,
mouse_y: 0,
mouse_event: 0,
key_code: 0,
key_down: false,
};
tx.send(command).unwrap();
0
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_closeConnection( _env: JNIEnv, _: JClass){
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::CloseConnection,
port: 0,
quality: 0,
compress: 0,
host: String::from(""),
pass: String::from(""),
hide_mouse: false,
mouse_x: 0,
mouse_y: 0,
mouse_event: 0,
key_code: 0,
key_down: false,
};
tx.send(command).unwrap();
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_stopUpdate( _env: JNIEnv, _: JClass){
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::StopUpdate,
port: 0,
quality: 0,
compress: 0,
host: String::from(""),
pass: String::from(""),
hide_mouse: false,
mouse_x: 0,
mouse_y: 0,
mouse_event: 0,
key_code: 0,
key_down: false,
};
tx.send(command).unwrap();
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_finish( _env: JNIEnv, _: JClass){
// vnc = NULL;
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::Quit,
port: 0,
quality: 0,
compress: 0,
host: String::from(""),
pass: String::from(""),
hide_mouse: false,
mouse_x: 0,
mouse_y: 0,
mouse_event: 0,
key_code: 0,
key_down: false,
};
tx.send(command).unwrap();
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_iniJNI( env: JNIEnv, this: JClass){
let rfb_client: Arc<Mutex<RfbClient>> = Arc::new(Mutex::new(RfbClient::new()));
let mut stop_server_connect = false;
thread::spawn(move || {
loop {
let client_arc = Arc::clone(&rfb_client);
let client = client_arc.lock().unwrap();
if client.get_connection_succeed() {
break;
}
thread::sleep(Duration::from_secs(1));
}
loop {
if stop_server_connect {
break;
}
}
});
let task = async {
let handle = thread::spawn(move || {
loop {
let command = COMMAND_CHANNEL.rx.lock().unwrap().recv().unwrap();
match command.command {
JniCommandNo::IniConnect => {
println!("Ini_connect");
vnc_ini_connection(
command.host,
command.port,
command.pass,
command.quality,
command.compress,
command.hide_mouse);
},
JniCommandNo::CloseConnection => {
vnc_close_connection();
},
JniCommandNo::MouseEvent => {
vnc_send_mouse_event(
command.mouse_x,
command.mouse_y,
command.mouse_event);
break;
},
JniCommandNo::KeyEvent => {
vnc_send_key_event(
command.key_code,
command.key_down);
break;
},
JniCommandNo::StopUpdate => {
vnc_set_update(false);
break;
},
JniCommandNo::Quit => {
stop_server_connect = true;
break;
},
}
}
});
handle.join().unwrap();
};
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(task);
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_MouseEvent(
_env: JNIEnv, _: JClass, x: jint ,y: jint ,event: jint) -> jboolean{
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::MouseEvent,
port: 0,
quality: 0,
compress: 0,
host: String::from(""),
pass: String::from(""),
hide_mouse: false,
mouse_x: x,
mouse_y: y,
mouse_event: event,
key_code: 0,
key_down: false,
};
tx.send(command).unwrap();
0
}
#[no_mangle]
pub extern "C" fn Java_net_miyataroid_vncrust_vncrust_KeyEvent(
_env: JNIEnv, _: JClass, key: jint ,down: jboolean ) -> jboolean {
let tx = COMMAND_CHANNEL.tx.lock().unwrap();
let command = JniCommand {
command: JniCommandNo::KeyEvent,
port: 0,
quality: 0,
compress: 0,
host: String::from(""),
pass: String::from(""),
hide_mouse: false,
mouse_x: 0,
mouse_y: 0,
mouse_event: 0,
key_code: key,
key_down: match down {
0 => true,
_ => false,
},
};
tx.send(command).unwrap();
0
}
fn get_string(env: &JNIEnv, string_value_ptr: JString, default_value: String) -> String {
let cstr = unsafe {
CStr::from_ptr(env.get_string(string_value_ptr).expect("invalid string").as_ptr())
};
match cstr.to_str() {
Err(_) =>{
default_value
},
Ok(string) => {
String::from(string)
},
}
}
struct JniCommandChannel {
tx: Mutex<Sender<JniCommand >>,
rx: Mutex<Receiver<JniCommand >>,
}
enum JniCommandNo {
IniConnect,
CloseConnection,
MouseEvent,
KeyEvent,
StopUpdate,
Quit,
}
struct JniCommand {
command: JniCommandNo,
port: i32,
quality: i32,
compress: i32,
host: String,
pass: String,
hide_mouse: bool,
mouse_x: i32,
mouse_y: i32,
mouse_event: i32,
key_code: i32,
key_down: bool,
}
}
間を作る
ここで詰まった。先ほどのlib.rsからiniJniだけ見てほしい。下記コードはビルドが通らない。
なぜならRfbClientがunsafeなので、Arc+Mutexの力を借りてもエラーになるのだ。
20分ほど悩んだがうまい解決方法が思い浮かばなかったのであきらめた。
let rfb_client: Arc<Mutex<RfbClient>> = Arc::new(Mutex::new(RfbClient::new()));
let mut stop_server_connect = false;
thread::spawn(move || {
loop {
let client_arc = Arc::clone(&rfb_client);
let client = client_arc.lock().unwrap();
if client.get_connection_succeed() {
break;
}
thread::sleep(Duration::from_secs(1));
}
loop {
if stop_server_connect {
break;
}
}
});
let task = async {
let handle = thread::spawn(move || {
loop {
let command = COMMAND_CHANNEL.rx.lock().unwrap().recv().unwrap();
match command.command {
JniCommandNo::IniConnect => {
println!("Ini_connect");
vnc_ini_connection(
command.host,
command.port,
command.pass,
command.quality,
command.compress,
command.hide_mouse);
},
JniCommandNo::CloseConnection => {
vnc_close_connection();
},
JniCommandNo::MouseEvent => {
vnc_send_mouse_event(
command.mouse_x,
command.mouse_y,
command.mouse_event);
break;
},
JniCommandNo::KeyEvent => {
vnc_send_key_event(
command.key_code,
command.key_down);
break;
},
JniCommandNo::StopUpdate => {
vnc_set_update(false);
break;
},
JniCommandNo::Quit => {
stop_server_connect = true;
break;
},
}
}
});
handle.join().unwrap();
};
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(task);
振り返り
一通りチュートリアルで学んだ気がしていたのだが、unsafeが混じってくると急に難しさが増してくる。
やっぱり何かしら一つ作らないとうまく呑み込めない気がする。
また、既存のAndroidJniをRustに変更しようとすると、必ずと言っていいほどCppとの連動が入ると思う。
これまた敷居高いので、見積もる時は慎重に。
Android NDKまわりをRustで実装した時の利点は
- ライブラリを完全に切り離せる
- Android Studioにコードを置かなくて済む
逆にRust下記点でちょっとアレ
- なんだかんだunsafe入る
- 結局C/CPP周りの(環境構築を含めた)知識は必須
それでは良いRustライフを!