eztrans-rs 0.2.1

FFI bindings and IPC server for ChangShinSoft's Eztrans software
# 멀티프로세스 유니코드 스캔 가이드


## 개요


EzTrans DLL을 사용한 유니코드 스캔에서 멀티프로세싱을 구현할 때 발견된 문제점과 해결책을 정리한 문서입니다.

## 왜 멀티프로세스인가? (Thread-Local 불가 이유)


### 테스트 결과 요약


| 접근법 | 결과 | 비고 |
|--------|------|------|
| 단일 스레드 순차 | ✅ 작동 | 기본 사용법 |
| 단일 스레드 다중 엔진 | ✅ 작동 | 같은 스레드에서 번갈아 사용 가능 |
| Thread-Local 엔진 | ❌ 불가 | LoadLibrary가 동일 HMODULE 반환 |
| Mutex 보호 다중 엔진 | ❌ 일부 실패 | DLL 전역 상태 충돌 |
| 멀티스레드 동시 접근 | ❌ 크래시 | Segfault, Heap Corruption |
| **멀티프로세스** |**작동** | 유일한 병렬화 방법 |

### DLL 내부 구조 분석


```
┌─────────────────────────────────────────────────────────────┐
│                    단일 프로세스 내부                        │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐   │
│  │         J2KEngine.dll 전역 상태                      │   │
│  │  - 초기화 플래그                                      │   │
│  │  - 사전 데이터                                        │   │
│  │  - 내부 번역 버퍼 (Thread-Unsafe!)                    │   │
│  └─────────────────────────────────────────────────────┘   │
│         ↑              ↑              ↑                     │
│         │              │              │                     │
│    ┌─────────┐    ┌─────────┐    ┌─────────┐              │
│    │Engine 1 │    │Engine 2 │    │Engine 3 │              │
│    │HMODULE: │    │HMODULE: │    │HMODULE: │              │
│    │0x648e0000│   │0x648e0000│   │0x648e0000│  ← 동일!    │
│    └─────────┘    └─────────┘    └─────────┘              │
│                                                             │
│    Thread 1        Thread 2        Thread 3                │
│    (번역 호출)     (번역 호출)     (번역 호출)              │
│         ↓              ↓              ↓                     │
│         └──────────────┼──────────────┘                     │
│                        ↓                                    │
│              💥 버퍼 충돌! 💥                              │
│              - 출력 혼합                                    │
│              - 빈 결과 반환                                 │
│              - Heap Corruption                             │
│              - Segmentation Fault                          │
└─────────────────────────────────────────────────────────────┘
```

### 핵심 발견사항


1. **LoadLibraryA 동작**: 이미 로드된 DLL에 대해 **동일한 HMODULE** 반환
   ```
   Engine 1 HMODULE: 0x648e0000
   Engine 2 HMODULE: 0x648e0000  ← 같은 핸들!
   ```

2. **DLL 전역 상태 공유**: Engine2를 초기화하지 않아도 Engine1의 초기화 상태로 번역 가능
   ```rust
   // Engine1만 초기화
   engine1.initialize_ex("CSUSER123455", &dat_path)?;

   // Engine2는 초기화 없이도 작동! (DLL 상태 공유)
   engine2.translate_mmntw("テスト")  // → "테스트" (작동함)
   ```

3. **동시 접근 시 충돌**: 번역 함수 내부 버퍼가 thread-safe하지 않음
   - 같은 스레드에서 순차 호출: ✅ 작동
   - 다른 스레드에서 동시 호출: ❌ 충돌

### 멀티프로세스가 유일한 해결책인 이유


```
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Process 1   │  │  Process 2   │  │  Process 3   │
├──────────────┤  ├──────────────┤  ├──────────────┤
│ J2KEngine.dll│  │ J2KEngine.dll│  │ J2KEngine.dll│
│ (독립 인스턴스)│  │ (독립 인스턴스)│  │ (독립 인스턴스)│
│              │  │              │  │              │
│ 전역 상태 A  │  │ 전역 상태 B  │  │ 전역 상태 C  │
│ 버퍼 A       │  │ 버퍼 B       │  │ 버퍼 C       │
└──────────────┘  └──────────────┘  └──────────────┘
       ↓                ↓                 ↓
    번역 결과 A      번역 결과 B      번역 결과 C
    (정상)          (정상)           (정상)
```

각 프로세스는:
- **완전히 독립된 메모리 공간**
- **독립된 DLL 인스턴스**
- **독립된 전역 상태 및 버퍼**

---

## 핵심 문제점 및 해결책


### 1. Rust 테스트 바이너리의 특성


Rust의 `cargo test`로 생성된 바이너리를 자식 프로세스로 실행하면, **전체 테스트 하네스가 실행**됩니다.

```
# 부모에서 자식 스폰 시 발생하는 문제

자식 프로세스 stdout:
  "running 3 tests"
  "test test_foo ... ok"
  ...
```

**해결책**: 환경변수로 워커 모드를 체크하고, 특정 테스트만 `--exact` 플래그로 실행

```rust
// 워커 전용 테스트
#[test]

fn worker_runner() {
    if let Ok(params) = env::var("WORKER_PARAMS") {
        run_worker(&params);
        std::process::exit(0);  // 즉시 종료
    }
}

// 코디네이터에서 스폰 시
Command::new(&current_exe)
    .env("WORKER_PARAMS", "...")
    .arg("worker_runner")  // 테스트 이름
    .arg("--exact")        // 정확히 이 테스트만
    .arg("--nocapture")    // stdout 캡처 안 함
    .spawn()
```

### 2. stdout 버퍼 블로킹


여러 워커의 stdout을 **순차적으로 blocking read**하면, 첫 번째 워커가 완료될 때까지 다른 워커들의 버퍼가 가득 차서 데드락 발생.

**해결책**: 스레드와 채널을 사용한 병렬 읽기

```rust
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

for worker in workers {
    let tx_clone = tx.clone();
    thread::spawn(move || {
        let reader = BufReader::new(worker.stdout);
        for line in reader.lines() {
            tx_clone.send(line).ok();
        }
    });
}
drop(tx);  // 원본 송신자 드롭

// 메인 스레드에서 모든 메시지 수신
for msg in rx {
    // 처리
}
```

### 3. DLL 동시 로드 충돌


여러 프로세스가 동시에 같은 DLL을 로드하면 충돌 가능.

**해결책**: 워커 스폰 시 약간의 지연, 또는 워커 내에서 worker_id 기반 지연

```rust
// 코디네이터에서
for (worker_id, ...) in work_assignments {
    spawn_worker(worker_id);
    thread::sleep(Duration::from_millis(100));  // 스폰 간 지연
}

// 또는 워커에서
fn run_worker(worker_id: usize) {
    thread::sleep(Duration::from_millis(worker_id as u64 * 500));
    let engine = EzTransEngine::new(&dll_path)?;
    // ...
}
```

### 4. 코디네이터에서 DLL 로드 후 크래시


코디네이터 프로세스에서 DLL을 로드/테스트한 후 워커를 스폰하면 ACCESS_VIOLATION 발생.

**해결책**: 코디네이터에서는 DLL을 로드하지 않음 (워커만 로드)

```rust
fn coordinator() {
    // DLL 테스트 생략 - 워커들이 각자 로드
    println!("Skipping DLL test in coordinator");

    // 워커 스폰
    for ... {
        spawn_worker(...);
    }
}
```

---

## 권장 아키텍처


```
┌─────────────────────────────────────────────────────────────┐
│                    Coordinator Process                       │
│  - DLL 로드 안 함                                            │
│  - 워커 스폰 (순차적, 약간의 지연)                            │
│  - mpsc 채널로 모든 워커 메시지 수신                          │
│  - 진행률 대시보드 표시                                       │
└─────────────────────────────────────────────────────────────┘
         │ spawn          │ spawn          │ spawn
         ▼                ▼                ▼
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  Worker 0   │   │  Worker 1   │   │  Worker 2   │
│  - DLL 로드  │   │  - DLL 로드  │   │  - DLL 로드  │
│  - 작업 수행 │   │  - 작업 수행 │   │  - 작업 수행 │
│  - JSON 출력 │   │  - JSON 출력 │   │  - JSON 출력 │
└─────────────┘   └─────────────┘   └─────────────┘
         │                │                │
         └────────────────┼────────────────┘
                          │ stdout (JSON)
              ┌───────────────────────┐
              │  Reader Threads       │
              │  (각 워커당 1개)       │
              └───────────────────────┘
                          │ mpsc channel
              ┌───────────────────────┐
              │  Main Loop            │
              │  - 메시지 처리         │
              │  - 진행률 업데이트     │
              │  - 결과 수집           │
              └───────────────────────┘
```

---

## 워커-코디네이터 프로토콜


JSON 기반 메시지 프로토콜:

```rust
#[derive(Serialize, Deserialize)]

enum WorkerMessage {
    Progress {
        worker_id: usize,
        current_code: u32,
        tested: u32,
        found: u32,
    },
    ChunkResult {
        worker_id: usize,
        problematic_chars: Vec<u32>,
    },
    Complete {
        worker_id: usize,
        total_tested: u32,
        total_found: u32,
        elapsed_secs: f64,
    },
    Error {
        worker_id: usize,
        message: String,
    },
}
```

---

## 대안 접근법 비교


### ❌ Thread-Local Storage (TLS)


```rust
thread_local! {
    static ENGINE: RefCell<Option<EzTransEngine>> = RefCell::new(None);
}
```

**불가 이유**: LoadLibrary가 동일 HMODULE 반환, DLL 전역 상태 공유

### ❌ Mutex 보호


```rust
let engine = Arc::new(Mutex::new(engine));
let guard = engine.lock().unwrap();
guard.translate_mmntw(text)?;
```

**문제점**: 병렬성 없음 (순차 실행과 동일), 일부 케이스에서 여전히 실패

### ❌ DLL 복사 후 다중 로드


```rust
// J2KEngine_1.dll, J2KEngine_2.dll 등으로 복사
let engine1 = EzTransEngine::new("J2KEngine_1.dll")?;
let engine2 = EzTransEngine::new("J2KEngine_2.dll")?;
```

**문제점**: 라이선스/리소스 문제 가능, 관리 복잡성 증가

### ✅ 멀티프로세스 (권장)


```rust
// 각 프로세스가 독립적으로 DLL 로드
fn worker_process() {
    let engine = EzTransEngine::new(&dll_path)?;
    engine.initialize_ex("CSUSER123455", &dat_path)?;
    // 번역 수행...
}
```

**장점**:
- 완전한 격리
- 안정적인 병렬 처리
- 한 워커 크래시가 다른 워커에 영향 없음

---

## 실행 방법


```bash
# V2 (개선된 멀티프로세스) - 자동 프로세스 수

cargo test --target i686-pc-windows-msvc --test full_unicode_scan \
    scan_entire_unicode_range_parallel_v2 -- --ignored --nocapture

# V2 - 8 프로세스 고정

cargo test --target i686-pc-windows-msvc --test full_unicode_scan \
    scan_entire_unicode_range_parallel_v2_8_procs -- --ignored --nocapture

# 소규모 테스트 (100개 문자)

cargo test --target i686-pc-windows-msvc --test full_unicode_scan_mini \
    test_mini_multiprocess -- --nocapture

# 중간 규모 테스트 (4000개 문자)

cargo test --target i686-pc-windows-msvc --test full_unicode_scan_mini \
    test_medium_multiprocess -- --ignored --nocapture
```

---

## 성능 참고


| 구성 | 처리량 | 비고 |
|------|--------|------|
| 단일 프로세스 | ~50-100 chars/sec | 기준선 |
| 4 프로세스 | ~200-400 chars/sec | 4배 향상 |
| 8 프로세스 | ~400-800 chars/sec | 8배 향상 |

- 전체 유니코드 (1,112,064 codepoints): 약 20-40분 예상 (8 프로세스 기준)
- CPU 코어 수에 따라 최적 프로세스 수 조정 권장

---

## 관련 파일


- `tests/full_unicode_scan.rs` - 전체 스캔 테스트 (V1, V2)
- `tests/full_unicode_scan_mini.rs` - 소규모/중간 규모 테스트
- `tests/multiprocess_test.rs` - 멀티프로세스 기본 패턴 테스트
- `tests/thread_safety_test.rs` - 스레드 안전성 테스트
- `tests/thread_local_test.rs` - Thread-Local 방식 테스트 (실패 케이스 기록)

---

## 테스트 재현 방법


Thread-Local 불가를 직접 확인하려면:

```bash
# 1. 단일 스레드 다중 엔진 (성공)

cargo test --target i686-pc-windows-msvc --test thread_local_test \
    test_single_thread_two_engines -- --ignored --nocapture

# 2. DLL 핸들 동일성 확인 (동일 HMODULE 반환 확인)

cargo test --target i686-pc-windows-msvc --test thread_local_test \
    test_dll_handle_identity -- --ignored --nocapture

# 3. 멀티스레드 동시 접근 (크래시 발생)

cargo test --target i686-pc-windows-msvc --test thread_local_test \
    test_thread_local_separate_engines -- --ignored --nocapture
```