From 76fd2f3cbfed95fa775026beee0679ac8281623e Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:19:05 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20HWP=20ABI=20=ED=8C=90=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20(=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80=EC=8B=9C?= =?UTF-8?q?=2010/20,=2028=20=EC=A7=80=EC=A0=81=20-=20=EC=98=9B=20.so=20?= =?UTF-8?q?=EC=97=90=20=EC=83=88=20=EC=9D=B8=EC=88=98=EB=A5=BC=20=EB=84=98?= =?UTF-8?q?=EA=B8=B0=EB=A9=B4=20=ED=9E=99=20=EC=86=90=EC=83=81):=20Rust=20?= =?UTF-8?q?jj=5Frhwp=5Fabi=5Fversion()=20=3D=202=20=C2=B7=20Dart=20?= =?UTF-8?q?=EB=8A=94=20=EC=B2=98=EC=9D=8C=20HWP=20=EB=A5=BC=20=EC=97=B4=20?= =?UTF-8?q?=EB=95=8C=20=EA=BC=AD=20=EC=9E=88=EC=96=B4=EC=95=BC=20=ED=95=A0?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=207=EA=B0=9C=EC=99=80=20=ED=8C=90?= =?UTF-8?q?=EC=9D=84=20=EB=B3=B4=EA=B3=A0=20=EB=8B=A4=EB=A5=B4=EB=A9=B4=20?= =?UTF-8?q?=EC=95=B1=20=EC=95=88=20=EB=B3=B4=EA=B8=B0=EB=A5=BC=20=EC=93=B0?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EB=8B=A4=EB=A5=B8=20=EC=95=B1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=C2=B7=20check=5Fapk=20=EA=B0=80=20APK=20?= =?UTF-8?q?=EC=95=88=20libjj=5Frhwp.so=20=EC=9D=98=20=EB=82=B4=EB=B3=B4?= =?UTF-8?q?=EB=82=B8=20=EA=B8=B0=ED=98=B8=EB=A5=BC=20NDK=20llvm-nm=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EA=B3=A0=20=EC=97=86=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20=EC=8B=A4=ED=8C=A8=20(986=20=EC=9D=98=20=EC=98=9B?= =?UTF-8?q?=20.so=20=EB=A5=BC=20=EC=9E=A1=EC=9D=8C)=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20hwp=5Fabi=5Ftest=20(=EA=B2=80=ED=86=A0=20=EB=8C=80?= =?UTF-8?q?=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lib/platform/android/hwp_pdf_ffi.dart | 30 +++++++++++++++++++++++---- test/hwp_abi_test.dart | 28 +++++++++++++++++++++++++ tool/check_apk.ps1 | 28 +++++++++++++++++++++++-- tool/rhwp_ffi/src/lib.rs | 10 +++++++++ 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 test/hwp_abi_test.dart diff --git a/lib/platform/android/hwp_pdf_ffi.dart b/lib/platform/android/hwp_pdf_ffi.dart index e0b679b..40e04ae 100644 --- a/lib/platform/android/hwp_pdf_ffi.dart +++ b/lib/platform/android/hwp_pdf_ffi.dart @@ -16,11 +16,33 @@ class RhwpEngine implements HwpPdfEngine { /// 관리자 판단 (10/20): 한글 글꼴 컴포넌트를 받으면 그 폴더를 앞에 더한다 (아직 없음) static const fontDirs = ['/system/fonts']; - /// 라이브러리를 읽을 수 있는지 (없으면 다른 앱으로 연다) - static bool get available { + /// 이 Dart 가 기대하는 C ABI 의 판 (tool/rhwp_ffi 의 JJ_RHWP_ABI_VERSION 과 같아야 함) + static const abiVersion = 2; + + /// 꼭 있어야 하는 C 함수 + static const requiredSymbols = [ + 'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_close', 'jj_rhwp_free', // + ]; + + static bool? _available; + + /// 라이브러리를 읽을 수 있고 ABI 가 맞는지 (아니면 다른 앱으로 연다). 처음 HWP 를 열 때 한 번 본다. + /// 관리자 지시 (10/20, 28 지적): 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가지므로, 기호와 판이 맞을 때만 + static bool get available => _available ??= () { + try { + final lib = DynamicLibrary.open(libName); + return abiCompatible( + has: lib.providesSymbol, abi: () => lib.lookupFunction('jj_rhwp_abi_version')()); + } catch (_) { + return false; + } + }(); + + /// [requiredSymbols] 가 모두 있고 [abi] 가 [abiVersion] 인지 (시험용으로 따로 - 기호를 찾다 · 부르다 실패하면 false) + static bool abiCompatible({required bool Function(String name) has, required int Function() abi}) { try { - DynamicLibrary.open(libName); - return true; + if (!requiredSymbols.every(has)) return false; + return abi() == abiVersion; } catch (_) { return false; } diff --git a/test/hwp_abi_test.dart b/test/hwp_abi_test.dart new file mode 100644 index 0000000..4336139 --- /dev/null +++ b/test/hwp_abi_test.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/platform/android/hwp_pdf_ffi.dart'; + +/// 관리자 지시 (10/20, 28 지적): 옛 libjj_rhwp.so (ABI 판 1 - jj_rhwp_cancel · jj_rhwp_abi_version 없음, 오류 code 칸 없음) 이면 +/// 앱 안 보기를 쓰지 않고 다른 앱으로 연다 (새 인수를 넘기면 힙이 망가진다) +void main() { + const v1 = {'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_close', 'jj_rhwp_free', 'jj_rhwp_version'}; + final v2 = {...v1, 'jj_rhwp_cancel', 'jj_rhwp_abi_version'}; + + test('옛 .so (기호 없음) · 판이 다름 · 부르다 실패 → 쓰지 않음, 맞으면 씀', () { + expect(RhwpEngine.abiCompatible(has: v1.contains, abi: () => throw ArgumentError('no symbol')), isFalse, reason: '판 1'); + expect(RhwpEngine.abiCompatible(has: {...v1, 'jj_rhwp_abi_version'}.contains, abi: () => 2), isFalse, reason: 'cancel 없음'); + expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => 1), isFalse, reason: '판이 다름'); + expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => 3), isFalse, reason: '더 새 판도'); + expect(RhwpEngine.abiCompatible(has: (_) => throw StateError('x'), abi: () => 2), isFalse); + expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => RhwpEngine.abiVersion), isTrue); + }); + + test('Dart 의 판 = tool/rhwp_ffi 의 JJ_RHWP_ABI_VERSION · 꼭 있어야 하는 함수는 lib.rs 에 모두', () { + final rs = File('tool/rhwp_ffi/src/lib.rs').readAsStringSync(); + expect(rs, contains('pub const JJ_RHWP_ABI_VERSION: c_int = ${RhwpEngine.abiVersion};')); + for (final s in RhwpEngine.requiredSymbols) { + expect(rs, contains('pub extern "C" fn $s('), reason: s); + } + }); +} diff --git a/tool/check_apk.ps1 b/tool/check_apk.ps1 index feb5825..8a0763f 100644 --- a/tool/check_apk.ps1 +++ b/tool/check_apk.ps1 @@ -8,6 +8,8 @@ # 새 버전 확인도 "최신과 같음" 이 된다. 시험판은 flutter build apk --build-number 9xx 로 빌드한다. 정식 판 APK 는 -Release. # - 순번 900 이상의 v 태그 (시험판 Pre-release, 예: v2026.10.18_977) 는 정식 판으로 보지 않는다 (관리자 지시 10/19, 28 지적: # 시험판에 태그를 단 뒤 같은 APK 를 다시 검사하면 "정식 판 번호와 같음" 으로 거짓 실패). 정식 판 (-Release) 인데 순번이 900 이상이면 실패. +# - HWP 보기 (관리자 지시 10/20, 28 지적): APK 안 libjj_rhwp.so 가 앱의 Dart 와 같은 C ABI 인지 - 내보낸 기호 (NDK llvm-nm) 에 +# jj_rhwp_cancel · jj_rhwp_abi_version 등이 모두 있어야 한다 (옛 .so 는 이름 · 목록만으로는 못 가린다). # 사용: powershell -ExecutionPolicy Bypass -File tool\check_apk.ps1 -Apk build\app\outputs\flutter-apk\app-release.apk [-Release] param( [Parameter(Mandatory = $true)][string]$Apk, @@ -41,7 +43,7 @@ if (-not $Libs) { $Libs = Join-Path $app 'android\app\third_party_libs.txt' } $listed = @(Get-Content $Libs | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') }) # third_party 에 있는 파일 (목록 밖이면 APK 에 없어야 함) $tp = @() -foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android', 'third_party\tor\android', 'third_party\7zip\android') { +foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android', 'third_party\tor\android', 'third_party\7zip\android', 'third_party\rhwp\android') { $dir = Join-Path $app $d if (Test-Path $dir) { $tp += @(Get-ChildItem $dir -Recurse -Filter *.so | ForEach-Object { $_.Name }) } } @@ -50,6 +52,13 @@ try { $libsIn = @($zip.Entries | Where-Object { $_.FullName -like 'lib/arm64-v8a/*.so' } | ForEach-Object { $_.Name }) $dexes = @($zip.Entries | Where-Object { $_.FullName -match '^classes\d*\.dex$' }) if ($dexes.Count -eq 0) { throw "dex 가 없습니다: $Apk" } + # HWP 보기 라이브러리는 꺼내 두고 아래에서 기호를 본다 + $rhwpSo = $null + $rhwpEntry = $zip.Entries | Where-Object { $_.FullName -eq 'lib/arm64-v8a/libjj_rhwp.so' } | Select-Object -First 1 + if ($rhwpEntry) { + $rhwpSo = Join-Path ([System.IO.Path]::GetTempPath()) ("jj_check_rhwp_{0}.so" -f [guid]::NewGuid().ToString('N')) + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($rhwpEntry, $rhwpSo) + } $found = $null foreach ($e in $dexes) { $s = $e.Open() @@ -61,6 +70,21 @@ try { $zip.Dispose() } $bad = @() +# HWP 보기: libjj_rhwp.so 의 내보낸 기호 (lib/platform/android/hwp_pdf_ffi.dart 의 requiredSymbols 와 같게) +$rhwpNote = '' +if ($rhwpSo) { + try { + $nm = Get-ChildItem (Join-Path $app '..\tools\android-sdk\ndk') -Recurse -Filter llvm-nm.exe -ErrorAction SilentlyContinue | Select-Object -Last 1 + if (-not $nm) { throw 'llvm-nm.exe 를 찾지 못했습니다 (tools\android-sdk\ndk)' } + $syms = @(& $nm.FullName -D --defined-only $rhwpSo 2>$null | ForEach-Object { ($_ -split '\s+')[-1] }) + $need = 'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_close', 'jj_rhwp_free' + $miss = @($need | Where-Object { $syms -notcontains $_ }) + if ($miss) { $bad += "libjj_rhwp.so 가 옛 C ABI 입니다 - 없는 함수: $($miss -join ', ') (sh tool/build_rhwp_android.sh 로 다시 빌드)" } + else { $rhwpNote = " · libjj_rhwp.so 함수 $($need.Count)개 있음" } + } finally { + Remove-Item $rhwpSo -Force -ErrorAction SilentlyContinue + } +} # 시험판 번호: aapt2 로 versionCode · versionName 을 읽어 정식 판 태그의 번호와 견준다 (build.gradle.kts 와 같은 셈: 년%100 · 월 · 일 · 번호) $aapt = Get-ChildItem (Join-Path $app '..\tools\android-sdk\build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1 if (-not $aapt -and $env:ANDROID_HOME) { $aapt = Get-ChildItem (Join-Path $env:ANDROID_HOME 'build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1 } @@ -90,4 +114,4 @@ if (-not $found) { Write-Host "실패: GeneratedPluginRegistrant 가 dex 에 없습니다 (켜자마자 꺼지는 APK). flutter pub get 뒤 --no-pub 없이 다시 빌드하세요." -ForegroundColor Red exit 1 } -Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음 · versionCode $code ($vname)$(if ($Release) { ' (정식 판)' } else { ' (시험판 - 정식 판 번호와 다름)' })" +Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음$rhwpNote · versionCode $code ($vname)$(if ($Release) { ' (정식 판)' } else { ' (시험판 - 정식 판 번호와 다름)' })" diff --git a/tool/rhwp_ffi/src/lib.rs b/tool/rhwp_ffi/src/lib.rs index c2975a3..d61fb71 100644 --- a/tool/rhwp_ffi/src/lib.rs +++ b/tool/rhwp_ffi/src/lib.rs @@ -100,6 +100,16 @@ fn open_kind(msg: &str, with_password: bool) -> c_int { } } +/// 이 C ABI 의 판 - Dart (lib/platform/android/hwp_pdf_ffi.dart 의 RhwpEngine.abiVersion) 와 같아야 앱 안 보기를 쓴다 +/// (관리자 지시 10/20, 28 지적: 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가진다). +/// 함수의 인수 · 오류 코드를 바꾸면 올린다 +pub const JJ_RHWP_ABI_VERSION: c_int = 2; + +#[no_mangle] +pub extern "C" fn jj_rhwp_abi_version() -> c_int { + JJ_RHWP_ABI_VERSION +} + /// 판 (예: "0.8.7") - 라이선스 화면 · 작업 기록용. jj_rhwp_free 로 푼다 #[no_mangle] pub extern "C" fn jj_rhwp_version() -> *mut c_char {