استفاده از FFI در پلاگین Flutter

1. مقدمه

دارت FFI (رابط عملکرد خارجی) به برنامه‌های Flutter اجازه می‌دهد از کتابخانه‌های بومی موجود که یک C API را در معرض نمایش می‌گذارند، استفاده کنند . دارت از FFI در Android، iOS، Windows، macOS و Linux پشتیبانی می کند. برای وب، دارت از interop جاوا اسکریپت پشتیبانی می کند، اما این موضوع در این کدنویسی پوشش داده نشده است.

چیزی که خواهی ساخت

در این کد لبه، شما یک افزونه موبایل و دسکتاپ می سازید که از کتابخانه C استفاده می کند. با این API، شما یک برنامه مثال ساده می نویسید که از افزونه استفاده می کند. افزونه و برنامه شما:

  • کد منبع کتابخانه C را به افزونه Flutter جدید خود وارد کنید
  • افزونه را سفارشی کنید تا به آن اجازه دهید روی Windows، macOS، Linux، Android و iOS ساخته شود
  • برنامه ای بسازید که از افزونه برای جاوا اسکریپت REPL استفاده کند (حلقه چاپ آشکار را بخوانید)

Duktape REPL در حال اجرا به عنوان یک برنامه macOS

چیزی که یاد خواهید گرفت

در این نرم افزار کد، دانش عملی مورد نیاز برای ساخت پلاگین Flutter مبتنی بر FFI را در هر دو پلت فرم دسکتاپ و موبایل، از جمله:

  • ایجاد یک قالب پلاگین دارت FFI مبتنی بر Flutter
  • استفاده از بسته ffigen برای تولید کد اتصال برای کتابخانه C
  • استفاده از CMake برای ساخت پلاگین Flutter FFI برای اندروید ، ویندوز و لینوکس
  • استفاده از CocoaPods برای ساخت پلاگین Flutter FFI برای iOS و macOS

آنچه شما نیاز دارید

  • Android Studio نسخه 4.1 یا بالاتر برای توسعه اندروید
  • Xcode 13 یا جدیدتر برای توسعه iOS و macOS
  • Visual Studio 2022 یا Visual Studio Build Tools 2022 با حجم کاری "Desktop Development with C++" برای توسعه دسکتاپ ویندوز
  • فلوتر SDK
  • هر گونه ابزار ساخت مورد نیاز برای پلتفرم هایی که در حال توسعه آن هستید (به عنوان مثال، CMake، CocoaPods و غیره).
  • LLVM برای پلتفرم هایی که روی آن ها توسعه خواهید داد . مجموعه ابزار کامپایلر LLVM توسط ffigen برای تجزیه فایل هدر C برای ساخت پیوند FFI در معرض دارت استفاده می شود.
  • یک ویرایشگر کد، مانند Visual Studio Code .

2. شروع به کار

ابزار ffigen اخیراً به Flutter اضافه شده است. با اجرای دستور زیر می توانید تأیید کنید که نصب Flutter شما نسخه پایدار فعلی را اجرا می کند.

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.3.9, on macOS 13.1 22C65 darwin-arm, locale en)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.2)
[✓] IntelliJ IDEA Community Edition (version 2022.2.2)
[✓] VS Code (version 1.74.0)
[✓] Connected device (2 available)
[✓] HTTP Host Availability

• No issues found!

تأیید کنید که خروجی flutter doctor بیان می‌کند که شما در کانال پایدار هستید و نسخه‌های جدیدتری فلاتر پایدار موجود نیست. اگر در حالت پایدار نیستید یا نسخه‌های جدیدتری در دسترس هستند، دو دستور زیر را اجرا کنید تا ابزار Flutter خود را به سرعت بالا ببرید.

$ flutter channel stable
$ flutter upgrade

می توانید کد را در این کد لبه با استفاده از هر یک از این دستگاه ها اجرا کنید:

  • کامپیوتر توسعه شما (برای ساخت دسکتاپ پلاگین و برنامه نمونه شما)
  • یک دستگاه فیزیکی Android یا iOS که به رایانه شما متصل شده و روی حالت Developer تنظیم شده است
  • شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد)
  • شبیه ساز اندروید (نیاز به راه اندازی در Android Studio دارد)

3. قالب پلاگین را ایجاد کنید

شروع با توسعه افزونه Flutter

Flutter با الگوهایی برای پلاگین هایی ارسال می شود که شروع به کار را آسان می کند. وقتی قالب افزونه را تولید می کنید، می توانید مشخص کنید که از کدام زبان می خواهید استفاده کنید.

دستور زیر را در پوشه کاری خود اجرا کنید تا پروژه خود را با استفاده از قالب پلاگین ایجاد کنید:

$ flutter create --template=plugin_ffi \
  --platforms=android,ios,linux,macos,windows ffigen_app

پارامتر --platforms مشخص می کند که پلاگین شما از کدام پلتفرم پشتیبانی می کند.

می توانید طرح پروژه تولید شده را با استفاده از دستور tree یا کاوشگر فایل سیستم عامل خود بررسی کنید.

$ tree -L 2 ffigen_app
ffigen_app
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
│   ├── build.gradle
│   ├── ffigen_app_android.iml
│   ├── local.properties
│   ├── settings.gradle
│   └── src
├── example
│   ├── README.md
│   ├── analysis_options.yaml
│   ├── android
│   ├── ffigen_app_example.iml
│   ├── ios
│   ├── lib
│   ├── linux
│   ├── macos
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── windows
├── ffigen.yaml
├── ffigen_app.iml
├── ios
│   ├── Classes
│   └── ffigen_app.podspec
├── lib
│   ├── ffigen_app.dart
│   └── ffigen_app_bindings_generated.dart
├── linux
│   └── CMakeLists.txt
├── macos
│   ├── Classes
│   └── ffigen_app.podspec
├── pubspec.lock
├── pubspec.yaml
├── src
│   ├── CMakeLists.txt
│   ├── ffigen_app.c
│   └── ffigen_app.h
└── windows
    └── CMakeLists.txt

17 directories, 26 files

ارزش آن را دارد که لحظه ای به ساختار دایرکتوری نگاه کنید تا احساس کنید چه چیزی ایجاد شده است و کجا قرار دارد. قالب plugin_ffi کد Dart را برای پلاگین در زیر دایرکتوری های lib ، پلتفرم خاص به نام های android ، ios ، linux ، macos ، و windows و مهمتر از همه، یک فهرست example قرار می دهد.

برای توسعه‌دهنده‌ای که به توسعه عادی Flutter عادت دارد، این ساختار ممکن است عجیب به نظر برسد، زیرا هیچ فایل اجرایی در سطح بالا تعریف نشده است. قرار است یک افزونه در سایر پروژه‌های Flutter گنجانده شود، اما برای اطمینان از کارکرد کد افزونه خود، کد را در فهرست راهنمای example وارد می‌کنید.

وقت آن است که شروع کنید!

4. مثال را بسازید و اجرا کنید

برای اطمینان از اینکه سیستم ساخت و پیش نیازها به درستی نصب شده اند و برای هر پلتفرم پشتیبانی شده کار می کنند، برنامه نمونه تولید شده را برای هر هدف بسازید و اجرا کنید.

ویندوز

اطمینان حاصل کنید که از نسخه پشتیبانی شده ویندوز استفاده می کنید. شناخته شده است که این کد لبه روی ویندوز 10 و ویندوز 11 کار می کند.

شما می توانید برنامه را از داخل ویرایشگر کد خود یا در خط فرمان بسازید.

PS C:\Users\brett\Documents> cd .\ffigen_app\example\
PS C:\Users\brett\Documents\ffigen_app\example> flutter run -d windows
Launching lib\main.dart on Windows in debug mode...Building Windows application...
Syncing files to device Windows...                                 160ms

Flutter run key commands.
r Hot reload.
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety

An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:53317/OiKWpyHXxHI=/
The Flutter DevTools debugger and profiler on Windows is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:53317/OiKWpyHXxHI=/

شما باید یک پنجره برنامه در حال اجرا مانند زیر را ببینید:

برنامه FFI ایجاد شده به عنوان یک برنامه ویندوز اجرا می شود

لینوکس

اطمینان حاصل کنید که از نسخه پشتیبانی شده لینوکس استفاده می کنید. این آزمایشگاه کد از Ubuntu 22.04.1 استفاده می کند.

هنگامی که تمام پیش نیازهای ذکر شده در مرحله 2 را نصب کردید، دستورات زیر را در ترمینال اجرا کنید:

$ cd ffigen_app/example
$ flutter run -d linux
Launching lib/main.dart on Linux in debug mode...
Building Linux application...
Syncing files to device Linux...                                   504ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:36653/Wgek1JGag48=/
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9103?uri=http://127.0.0.1:36653/Wgek1JGag48=/

شما باید یک پنجره برنامه در حال اجرا مانند زیر را ببینید:

برنامه FFI ایجاد شده توسط الگو در حال اجرا به عنوان یک برنامه لینوکس

اندروید

برای اندروید می توانید از Windows، macOS یا Linux برای کامپایل استفاده کنید. ابتدا مطمئن شوید که یک دستگاه اندرویدی به رایانه توسعه‌دهنده خود متصل هستید یا از یک نمونه شبیه‌ساز اندروید (AVD) استفاده می‌کنید. با اجرای موارد زیر تأیید کنید که Flutter می‌تواند به دستگاه Android یا شبیه‌ساز متصل شود:

$ flutter devices
3 connected devices:

sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 12 (API 32) (emulator)
macOS (desktop)             • macos         • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)                • chrome        • web-javascript • Google Chrome 108.0.5359.98

برنامه FFI ایجاد شده در یک شبیه ساز اندروید اجرا می شود

macOS و iOS

برای توسعه macOS و iOS Flutter، باید از رایانه macOS استفاده کنید.

با اجرای برنامه مثال در macOS شروع کنید. دوباره دستگاه هایی را که Flutter می بیند تأیید کنید:

$ flutter devices
2 connected devices:

macOS (desktop) • macos  • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)    • chrome • web-javascript • Google Chrome 108.0.5359.98

برنامه نمونه را با استفاده از پروژه پلاگین تولید شده اجرا کنید:

$ cd ffigen_app/example
$ flutter run -d macos

شما باید یک پنجره برنامه در حال اجرا مانند زیر را ببینید:

برنامه FFI ایجاد شده توسط الگو در حال اجرا به عنوان یک برنامه لینوکس

برای iOS می توانید از شبیه ساز یا یک دستگاه سخت افزار واقعی استفاده کنید. اگر از شبیه ساز استفاده می کنید، ابتدا شبیه ساز را راه اندازی کنید. دستور flutter devices اکنون شبیه ساز را به عنوان یکی از دستگاه های موجود فهرست می کند.

$ flutter devices
3 connected devices:

iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios            • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator)
macOS (desktop)                     • macos                                • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)                        • chrome                               • web-javascript • Google Chrome 108.0.5359.98

هنگامی که شبیه ساز شروع به کار کرد، اجرا کنید: flutter run .

$ cd ffigen_app/example
$ flutter run -d iphone

برنامه FFI ایجاد شده در یک شبیه ساز iOS اجرا می شود

شبیه‌ساز iOS بر هدف macOS اولویت دارد، بنابراین می‌توانید از تعیین دستگاهی با پارامتر -d صرفنظر کنید.

تبریک می گوییم، شما با موفقیت برنامه ای را روی پنج سیستم عامل مختلف ساخته و اجرا کرده اید. در مرحله بعد، پلاگین بومی را بسازید و با استفاده از FFI با آن از Dart ارتباط برقرار کنید.

5. استفاده از Duktape در ویندوز، لینوکس و اندروید

کتابخانه C که در این کد لبه استفاده خواهید کرد Duktape است. Duktape یک موتور جاوا اسکریپت قابل جاسازی با تمرکز بر قابلیت حمل و جابجایی فشرده است. در این مرحله، پلاگین را طوری پیکربندی می‌کنید که کتابخانه Duktape را کامپایل کرده، آن را به پلاگین خود پیوند دهید و سپس با استفاده از Dart's FFI به آن دسترسی پیدا کنید.

این مرحله ادغام را برای کار در ویندوز، لینوکس و اندروید پیکربندی می کند. ادغام iOS و macOS به پیکربندی اضافی (فراتر از آنچه در این مرحله توضیح داده شده است) نیاز دارد تا کتابخانه کامپایل شده را در فایل اجرایی Flutter نهایی قرار دهد. پیکربندی اضافی مورد نیاز در مرحله بعد پوشش داده شده است.

بازیابی Duktape

ابتدا یک کپی از کد منبع duktape را با دانلود آن از وب سایت duktape.org دریافت کنید.

برای ویندوز می توانید از PowerShell با Invoke-WebRequest استفاده کنید:

PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz

برای لینوکس، wget انتخاب خوبی است.

$ wget https://duktape.org/duktape-2.7.0.tar.xz
--2022-12-22 16:21:39--  https://duktape.org/duktape-2.7.0.tar.xz
Resolving duktape.org (duktape.org)... 104.198.14.52
Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1026524 (1002K) [application/x-xz]
Saving to: ‘duktape-2.7.0.tar.xz'

duktape-2.7.0.tar.x 100%[===================>]   1002K  1.01MB/s    in 1.0s

2022-12-22 16:21:41 (1.01 MB/s) - ‘duktape-2.7.0.tar.xz' saved [1026524/1026524]

فایل یک آرشیو tar.xz است. در ویندوز، یکی از گزینه ها این است که ابزار 7Zip را دانلود کنید و از آن به صورت زیر استفاده کنید.

PS> 7z x .\duktape-2.7.0.tar.xz

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 1026524 bytes (1003 KiB)

Extracting archive: .\duktape-2.7.0.tar.xz
--
Path = .\duktape-2.7.0.tar.xz
Type = xz
Physical Size = 1026524
Method = LZMA2:26 CRC64
Streams = 1
Blocks = 1

Everything is Ok

Size:       19087360
Compressed: 1026524

شما باید 7z را دو بار اجرا کنید، اول برای حذف فشرده سازی xz، دوم برای گسترش آرشیو tar.

PS> 7z x .\duktape-2.7.0.tar

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 19087360 bytes (19 MiB)

Extracting archive: .\duktape-2.7.0.tar
--
Path = .\duktape-2.7.0.tar
Type = tar
Physical Size = 19087360
Headers Size = 543232
Code Page = UTF-8
Characteristics = GNU ASCII

Everything is Ok

Folders: 46
Files: 1004
Size:       18281564
Compressed: 19087360

در محیط های لینوکس مدرن، tar محتویات را در یک مرحله به صورت زیر استخراج می کند.

$ tar xvf duktape-2.7.0.tar.xz
x duktape-2.7.0/
x duktape-2.7.0/README.rst
x duktape-2.7.0/Makefile.sharedlibrary
x duktape-2.7.0/Makefile.coffee
x duktape-2.7.0/extras/
x duktape-2.7.0/extras/README.rst
x duktape-2.7.0/extras/module-node/
x duktape-2.7.0/extras/module-node/README.rst
x duktape-2.7.0/extras/module-node/duk_module_node.h
x duktape-2.7.0/extras/module-node/Makefile
[... and many more files]

نصب LLVM

برای استفاده از ffigen ، باید LLVM را نصب کنید ، که ffigen از آن برای تجزیه هدرهای C استفاده می کند. در ویندوز دستور زیر را اجرا کنید.

PS> winget install -e --id LLVM.LLVM
Found LLVM [LLVM.LLVM] Version 15.0.5
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.5/LLVM-15.0.5-win64.exe
  ██████████████████████████████   277 MB /  277 MB
Successfully verified installer hash
Starting package install...
Successfully installed

مسیرهای سیستم خود را برای اضافه کردن C:\Program Files\LLVM\bin به مسیر جستجوی باینری خود پیکربندی کنید تا نصب LLVM بر روی دستگاه ویندوز شما تکمیل شود. می توانید به صورت زیر تست کنید که آیا به درستی نصب شده است.

PS> clang --version
clang version 15.0.5
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin

برای اوبونتو، وابستگی LLVM را می توان به صورت زیر نصب کرد. سایر توزیع های لینوکس وابستگی های مشابهی برای LLVM و Clang دارند.

$ sudo apt install libclang-dev
[sudo] password for brett:
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  libclang-15-dev
The following NEW packages will be installed:
  libclang-15-dev libclang-dev
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 26.1 MB of archives.
After this operation, 260 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-15-dev amd64 1:15.0.2-1 [26.1 MB]
Get:2 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-dev amd64 1:15.0-55.1ubuntu1 [2962 B]
Fetched 26.1 MB in 7s (3748 kB/s)
Selecting previously unselected package libclang-15-dev.
(Reading database ... 85898 files and directories currently installed.)
Preparing to unpack .../libclang-15-dev_1%3a15.0.2-1_amd64.deb ...
Unpacking libclang-15-dev (1:15.0.2-1) ...
Selecting previously unselected package libclang-dev.
Preparing to unpack .../libclang-dev_1%3a15.0-55.1ubuntu1_amd64.deb ...
Unpacking libclang-dev (1:15.0-55.1ubuntu1) ...
Setting up libclang-15-dev (1:15.0.2-1) ...
Setting up libclang-dev (1:15.0-55.1ubuntu1) ...

همانطور که در بالا ذکر شد، می توانید نصب LLVM خود را در لینوکس به صورت زیر تست کنید.

$ clang --version
Ubuntu clang version 15.0.2-1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

پیکربندی ffigen

الگوی ایجاد شده در سطح بالا pubpsec.yaml ممکن است دارای نسخه های قدیمی بسته ffigen باشد. دستور زیر را برای به روز رسانی وابستگی های Dart در پروژه پلاگین اجرا کنید:

$ flutter pub upgrade --major-versions

اکنون که بسته ffigen به روز است، در مرحله بعد پیکربندی کنید که ffigen کدام فایل ها را برای تولید فایل های binding مصرف کند. محتویات فایل ffigen.yaml پروژه خود را با موارد زیر تغییر دهید.

ffigen.yaml

# Run with `flutter pub run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
  Bindings for `src/duktape.h`.

  Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
output: 'lib/duktape_bindings_generated.dart'
headers:
  entry-points:
    - 'src/duktape.h'
  include-directives:
    - 'src/duktape.h'
preamble: |
  // ignore_for_file: always_specify_types
  // ignore_for_file: camel_case_types
  // ignore_for_file: non_constant_identifier_names
comments:
  style: any
  length: full
ignore-source-errors: true

این پیکربندی شامل فایل هدر C برای ارسال به LLVM، فایل خروجی برای تولید، توضیحاتی که در بالای فایل قرار می‌گیرد و یک بخش مقدمه برای افزودن یک هشدار پرز استفاده می‌شود.

یک مورد پیکربندی در انتهای فایل وجود دارد که شایسته توضیح بیشتر است. از نسخه 11.0.0 ffigen در صورت وجود اخطارها یا خطاهایی که هنگام تجزیه فایل‌های سرصفحه توسط clang ایجاد می‌شود، مولد binding به‌طور پیش‌فرض اتصال ایجاد نمی‌کند.

فایل‌های هدر Duktape، همانطور که نوشته شده‌اند، به دلیل عدم وجود مشخص‌کننده‌های نوع پوچ‌پذیری در نشانگرهای Duktape، باعث ایجاد clang در macOS می‌شوند. برای پشتیبانی کامل از macOS و iOS، Duktape به این نوع مشخص‌کننده‌ها نیاز دارد که به پایگاه کد Duktape اضافه شوند. در همین حال، با تنظیم پرچم ignore-source-errors روی true ، تصمیم می گیریم این هشدارها را نادیده بگیریم.

در یک برنامه تولیدی، باید قبل از ارسال برنامه، تمام هشدارهای کامپایلر را حذف کنید. با این حال، انجام این کار برای Duktape خارج از محدوده این Codelab است.

برای جزئیات بیشتر در مورد سایر کلیدها و مقادیر به مستندات ffigen مراجعه کنید.

شما باید فایل های Duktape خاصی را از توزیع Duktape در محلی که ffigen پیکربندی شده است برای پیدا کردن آنها کپی کنید.

$ cp duktape-2.7.0/src/duktape.c src/
$ cp duktape-2.7.0/src/duktape.h src/
$ cp duktape-2.7.0/src/duk_config.h src/

از نظر فنی، شما فقط باید در سراسر duktape.h برای ffigen کپی کنید، اما می‌خواهید CMake را برای ساخت کتابخانه‌ای که به هر سه نیاز دارد، پیکربندی کنید. برای تولید اتصال جدید، ffigen اجرا کنید:

$ flutter pub run ffigen --config ffigen.yaml
Running in Directory: '/home/brett/GitHub/codelabs/ffigen_codelab/step_05'
Input Headers: [./src/duktape.h]
[WARNING]: No definition found for declaration - (Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: No definition found for declaration - (Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: Generated declaration '__va_list_tag' start's with '_' and therefore will be private.
Finished, Bindings generated in /home/brett/GitHub/codelabs/ffigen_codelab/step_05/./lib/duktape_bindings_generated.dart

در هر سیستم عامل هشدارهای مختلفی را مشاهده خواهید کرد. فعلاً می‌توانید این موارد را نادیده بگیرید، زیرا Duktape 2.7.0 به عنوان کامپایل با clang در ویندوز، لینوکس و macOS شناخته شده است.

در حال پیکربندی CMake

CMake یک سیستم تولید سیستم ساخت است. این افزونه از CMake برای تولید سیستم ساخت اندروید، ویندوز و لینوکس استفاده می‌کند تا Duktape را در باینری Flutter تولید شده قرار دهد. شما باید فایل پیکربندی CMake ایجاد شده را به صورت زیر تغییر دهید.

src/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(ffigen_app_library VERSION 0.0.1 LANGUAGES C)

add_library(ffigen_app SHARED
  duktape.c                     # Modify
)

set_target_properties(ffigen_app PROPERTIES
  PUBLIC_HEADER duktape.h       # Modify
  PRIVATE_HEADER duk_config.h   # Add
  OUTPUT_NAME "ffigen_app"      # Add
)

# Add from here...
if (WIN32)
set_target_properties(ffigen_app PROPERTIES
  WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif (WIN32)
# ... to here.

target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)

پیکربندی CMake فایل‌های منبع را اضافه می‌کند و مهم‌تر از آن، رفتار پیش‌فرض فایل کتابخانه‌ای تولید شده در ویندوز را تغییر می‌دهد تا همه نمادهای C را به‌طور پیش‌فرض صادر کند. این یک کار CMake برای کمک به انتقال کتابخانه‌های سبک یونیکس، که Duktape است، به دنیای ویندوز است.

محتوای lib/ffigen_app.dart را با موارد زیر جایگزین کنید.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
  if (Platform.isMacOS || Platform.isIOS) {
    return DynamicLibrary.open('$_libName.framework/$_libName');
  }
  if (Platform.isAndroid || Platform.isLinux) {
    return DynamicLibrary.open('lib$_libName.so');
  }
  if (Platform.isWindows) {
    return DynamicLibrary.open('$_libName.dll');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
  Duktape() {
    ctx =
        _bindings.duk_create_heap(nullptr, nullptr, nullptr, nullptr, nullptr);
  }

  void evalString(String jsCode) {
    var nativeUtf8 = jsCode.toNativeUtf8();
    _bindings.duk_eval_raw(
        ctx,
        nativeUtf8.cast<Char>(),
        0,
        0 |
            DUK_COMPILE_EVAL |
            DUK_COMPILE_SAFE |
            DUK_COMPILE_NOSOURCE |
            DUK_COMPILE_STRLEN |
            DUK_COMPILE_NOFILENAME);
    ffi.malloc.free(nativeUtf8);
  }

  int getInt(int index) {
    return _bindings.duk_get_int(ctx, index);
  }

  void dispose() {
    _bindings.duk_destroy_heap(ctx);
    ctx = nullptr;
  }

  late Pointer<duk_hthread> ctx;
}

این فایل مسئول بارگیری فایل کتابخانه پیوند پویا ( .so برای لینوکس و اندروید، .dll برای ویندوز) و ارائه پوششی است که یک رابط اصطلاحی Dart را در معرض کد C زیرین قرار می دهد.

از آنجایی که این فایل مستقیماً بسته ffi را وارد می کند، باید بسته را از dev_dependencies به dependencies منتقل کنید. یک راه آسان برای انجام این کار اجرای دستور زیر است:

$ dart pub add ffi

محتویات main.dart مثال را با عبارت زیر جایگزین کنید.

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';

const String jsCode = '1+2';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late Duktape duktape;
  String output = '';

  @override
  void initState() {
    super.initState();
    duktape = Duktape();
    setState(() {
      output = 'Initialized Duktape';
    });
  }

  @override
  void dispose() {
    duktape.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    const textStyle = TextStyle(fontSize: 25);
    const spacerSmall = SizedBox(height: 10);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Duktape Test'),
        ),
        body: Center(
          child: Container(
            padding: const EdgeInsets.all(10),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  output,
                  style: textStyle,
                  textAlign: TextAlign.center,
                ),
                spacerSmall,
                ElevatedButton(
                  child: const Text('Run JavaScript'),
                  onPressed: () {
                    duktape.evalString(jsCode);
                    setState(() {
                      output = '$jsCode => ${duktape.getInt(-1)}';
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

اکنون می توانید برنامه نمونه را دوباره با استفاده از:

$ cd example
$ flutter run

باید ببینید که برنامه به این صورت اجرا می شود:

نمایش Duktape اولیه در یک برنامه ویندوز

نمایش خروجی جاوا اسکریپت Duktape در یک برنامه ویندوز

این دو اسکرین شات قبل و بعد از فشار دادن دکمه Run JavaScript را نشان می دهد. این نشان دهنده اجرای کد جاوا اسکریپت از دارت و نمایش نتیجه روی صفحه است.

اندروید

اندروید یک سیستم عامل لینوکس مبتنی بر هسته است و تا حدودی شبیه به توزیع های لینوکس دسکتاپ است. سیستم ساخت CMake می تواند بیشتر تفاوت های این دو پلتفرم را پنهان کند. برای ساخت و اجرا در اندروید، مطمئن شوید شبیه ساز اندروید در حال اجرا است (یا دستگاه اندروید متصل است). برنامه را اجرا کنید. به عنوان مثال:

$ cd example
$ flutter run -d emulator-5554

اکنون باید نمونه برنامه در حال اجرا در اندروید را مشاهده کنید:

نمایش Duktape اولیه در شبیه ساز اندروید

نمایش خروجی جاوا اسکریپت Duktape در شبیه ساز اندروید

6. استفاده از Duktape در macOS و iOS

اکنون زمان آن رسیده است که افزونه خود را روی macOS و iOS، دو سیستم عامل نزدیک به هم، کار کنید. با macOS شروع کنید. در حالی که CMake از macOS و iOS پشتیبانی می‌کند، از کارهایی که برای لینوکس و اندروید انجام داده‌اید، دوباره استفاده نخواهید کرد، زیرا Flutter در macOS و iOS از CocoaPods برای وارد کردن کتابخانه‌ها استفاده می‌کند.

تمیز کردن

در مرحله قبل شما یک برنامه کاربردی برای اندروید، ویندوز و لینوکس ساختید. با این حال، چند فایل از قالب اصلی باقی مانده است که اکنون باید آنها را تمیز کنید. اکنون آنها را به شرح زیر حذف کنید.

$ rm src/ffigen_app.c
$ rm src/ffigen_app.h
$ rm ios/Classes/ffigen_app.c
$ rm macos/Classes/ffigen_app.c

macOS

Flutter در پلتفرم macOS از CocoaPods برای وارد کردن کدهای C و C++ استفاده می‌کند. این بدان معنی است که این بسته باید در زیرساخت ساخت CocoaPods ادغام شود. برای فعال کردن استفاده مجدد از کد C که قبلاً برای ساخت با CMake در مرحله قبل پیکربندی کرده‌اید، باید یک فایل انتقال واحد را در رانر پلتفرم macOS اضافه کنید.

macos/Classes/duktape.c

#include "../../src/duktape.c"

این فایل از قدرت پیش پردازنده C برای گنجاندن کد منبع از کد منبع بومی که در مرحله قبل تنظیم کردید استفاده می کند. برای جزئیات بیشتر در مورد نحوه عملکرد، به macos/ffigen_app.podspec مراجعه کنید.

اجرای این برنامه اکنون از همان الگوی پیروی می کند که در ویندوز و لینوکس دیده اید.

$ cd example
$ flutter run -d macos

نمایش Duktape اولیه در یک برنامه macOS

نمایش خروجی جاوا اسکریپت Duktape در یک برنامه macOS

iOS

مشابه راه‌اندازی macOS، iOS نیاز به یک فایل انتقال C نیز دارد.

ios/Classes/duktape.c

#include "../../src/duktape.c"

با این فایل واحد، افزونه شما اکنون برای اجرا در iOS نیز پیکربندی شده است. آن را طبق معمول اجرا کنید.

$ flutter run -d iPhone

نمایش Duktape اولیه در شبیه ساز iOS

نمایش خروجی جاوا اسکریپت Duktape در شبیه ساز iOS

تبریک می گویم! شما با موفقیت کد بومی را در پنج پلتفرم ادغام کرده اید. این زمینه ای برای یک جشن است! شاید حتی یک رابط کاربری کاربردی تر، که در مرحله بعدی خواهید ساخت.

7. حلقه Read Eval Print را اجرا کنید

تعامل با یک زبان برنامه نویسی در یک محیط تعاملی سریع بسیار سرگرم کننده است. پیاده سازی اولیه چنین محیطی حلقه Print Eval Print (REPL) LISP بود. شما قصد دارید در این مرحله چیزی مشابه با Duktape پیاده سازی کنید.

آماده کردن تولید چیزها

کد فعلی که با کتابخانه Duktape C در تعامل است، فرض می‌کند که هیچ مشکلی پیش نمی‌آید. اوه، و کتابخانه های پیوند پویا Duktape را در هنگام آزمایش بارگیری نمی کند. برای آماده کردن این تولید یکپارچه، باید چند تغییر در lib/ffigen_app.dart ایجاد کنید.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p;             // Add this import

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
  if (Platform.isMacOS || Platform.isIOS) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return DynamicLibrary.open('build/macos/Build/Products/Debug'
          '/$_libName/$_libName.framework/$_libName');
    }
    // ...to here.
    return DynamicLibrary.open('$_libName.framework/$_libName');
  }
  if (Platform.isAndroid || Platform.isLinux) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return DynamicLibrary.open(
          'build/linux/x64/debug/bundle/lib/lib$_libName.so');
    }
    // ...to here.
    return DynamicLibrary.open('lib$_libName.so');
  }
  if (Platform.isWindows) {
    // Add from here...
    if (Platform.environment.containsKey('FLUTTER_TEST')) {
      return DynamicLibrary.open(p.canonicalize(
          p.join(r'build\windows\runner\Debug', '$_libName.dll')));
    }
    // ...to here.
    return DynamicLibrary.open('$_libName.dll');
  }
  throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
  Duktape() {
    ctx =
        _bindings.duk_create_heap(nullptr, nullptr, nullptr, nullptr, nullptr);
  }

  // Modify this function
  String evalString(String jsCode) {
    var nativeUtf8 = jsCode.toNativeUtf8();
    final evalResult = _bindings.duk_eval_raw(
        ctx,
        nativeUtf8.cast<Char>(),
        0,
        0 |
            DUK_COMPILE_EVAL |
            DUK_COMPILE_SAFE |
            DUK_COMPILE_NOSOURCE |
            DUK_COMPILE_STRLEN |
            DUK_COMPILE_NOFILENAME);
    ffi.malloc.free(nativeUtf8);

    if (evalResult != 0) {
      throw _retrieveTopOfStackAsString();
    }

    return _retrieveTopOfStackAsString();
  }

  // Add this function
  String _retrieveTopOfStackAsString() {
    Pointer<Size> outLengthPtr = ffi.calloc<Size>();
    final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
    final returnVal =
        errorStrPtr.cast<ffi.Utf8>().toDartString(length: outLengthPtr.value);
    ffi.calloc.free(outLengthPtr);
    return returnVal;
  }

  void dispose() {
    _bindings.duk_destroy_heap(ctx);
    ctx = nullptr;
  }

  late Pointer<duk_hthread> ctx;
}

کد بارگیری کتابخانه پیوند پویا برای رسیدگی به مواردی که افزونه در یک اجراکننده آزمایشی استفاده می شود، گسترش یافته است. این امکان را می‌دهد تا یک تست یکپارچه‌سازی نوشته شود که این API را به‌عنوان یک تست Flutter اجرا می‌کند. کد ارزیابی یک رشته کد جاوا اسکریپت برای کنترل صحیح شرایط خطا، به عنوان مثال کد ناقص یا نادرست، گسترش یافته است. این کد اضافی نحوه رسیدگی به موقعیت‌هایی را نشان می‌دهد که رشته‌ها به عنوان آرایه‌های بایتی برگردانده می‌شوند و باید به رشته‌های دارت تبدیل شوند.

اضافه کردن بسته ها

در ایجاد یک REPL، تعامل بین کاربر و موتور جاوا اسکریپت Duktape را نمایش می دهید. کاربر خطوط کد را وارد می کند و Duktape یا با نتیجه محاسبه یا یک استثنا پاسخ می دهد. برای کاهش مقدار کد دیگ بخاری که باید بنویسید، از freezed استفاده می کنید. همچنین از google_fonts برای اینکه محتوای نمایش داده شده را کمی بیشتر به موضوع تبدیل کنید و flutter_riverpod برای مدیریت حالت استفاده خواهید کرد.

وابستگی های مورد نیاز را به برنامه مثال اضافه کنید:

$ cd example
$ dart pub add flutter_riverpod freezed_annotation google_fonts
$ dart pub add -d build_runner freezed

سپس، یک فایل برای ضبط تعامل REPL ایجاد کنید:

example/lib/duktape_message.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'duktape_message.freezed.dart';

@freezed
class DuktapeMessage with _$DuktapeMessage {
  factory DuktapeMessage.evaluate(String code) = DuktapeMessageCode;
  factory DuktapeMessage.response(String result) = DuktapeMessageResponse;
  factory DuktapeMessage.error(String log) = DuktapeMessageError;
}

این کلاس از ویژگی freezed 's Union type استفاده می کند تا بیان آسان شکل هر خط نمایش داده شده در REPL به عنوان یکی از سه نوع را امکان پذیر کند. در این مرحله، کد شما احتمالاً اشکالی از خطا را در این کد نشان می دهد، زیرا کد اضافی وجود دارد که باید ایجاد شود. اکنون این کار را به صورت زیر انجام دهید.

$ flutter pub run build_runner build

این فایل example/lib/duktape_message.freezed.dart را ایجاد می کند که کدی که شما تایپ کردید به آن متکی است.

در مرحله بعد، باید یک جفت اصلاح در فایل‌های پیکربندی macOS انجام دهید تا google_fonts فعال کنید تا درخواست‌های شبکه برای داده‌های فونت را ارسال کند.

example/macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

example/macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

ساختن REPL

اکنون که لایه ادغام را برای رسیدگی به خطاها به‌روزرسانی کرده‌اید، و یک نمایش داده برای تعامل ایجاد کرده‌اید، وقت آن رسیده است که رابط کاربری برنامه نمونه را بسازید.

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';

import 'duktape_message.dart';

void main() {
  runApp(const ProviderScope(child: DuktapeApp()));
}

final duktapeMessagesProvider =
    StateNotifierProvider<DuktapeMessageNotifier, List<DuktapeMessage>>((ref) {
  return DuktapeMessageNotifier(messages: <DuktapeMessage>[]);
});

class DuktapeMessageNotifier extends StateNotifier<List<DuktapeMessage>> {
  DuktapeMessageNotifier({required List<DuktapeMessage> messages})
      : duktape = Duktape(),
        super(messages);
  final Duktape duktape;

  void eval(String code) {
    state = [
      DuktapeMessage.evaluate(code),
      ...state,
    ];
    try {
      final response = duktape.evalString(code);
      state = [
        DuktapeMessage.response(response),
        ...state,
      ];
    } catch (e) {
      state = [
        DuktapeMessage.error('$e'),
        ...state,
      ];
    }
  }
}

class DuktapeApp extends StatelessWidget {
  const DuktapeApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Duktape App',
      home: DuktapeRepl(),
    );
  }
}

class DuktapeRepl extends ConsumerStatefulWidget {
  const DuktapeRepl({
    super.key,
  });

  @override
  ConsumerState<DuktapeRepl> createState() => _DuktapeReplState();
}

class _DuktapeReplState extends ConsumerState<DuktapeRepl> {
  final _controller = TextEditingController();
  final _focusNode = FocusNode();
  var _isComposing = false;

  void _handleSubmitted(String text) {
    _controller.clear();
    setState(() {
      _isComposing = false;
    });
    setState(() {
      ref.read(duktapeMessagesProvider.notifier).eval(text);
    });
    _focusNode.requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    final messages = ref.watch(duktapeMessagesProvider);
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text('Duktape REPL'),
        elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
      ),
      body: Column(
        children: [
          Flexible(
            child: Ink(
              color: Theme.of(context).scaffoldBackgroundColor,
              child: SafeArea(
                bottom: false,
                child: ListView.builder(
                  padding: const EdgeInsets.all(8.0),
                  reverse: true,
                  itemBuilder: (context, idx) => messages[idx].when(
                    evaluate: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        '> $str',
                        style: GoogleFonts.firaCode(
                          textStyle: Theme.of(context).textTheme.titleMedium,
                        ),
                      ),
                    ),
                    response: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        '= $str',
                        style: GoogleFonts.firaCode(
                          textStyle: Theme.of(context).textTheme.titleMedium,
                          color: Colors.blue[800],
                        ),
                      ),
                    ),
                    error: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        str,
                        style: GoogleFonts.firaCode(
                          textStyle: Theme.of(context).textTheme.titleSmall,
                          color: Colors.red[800],
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                  itemCount: messages.length,
                ),
              ),
            ),
          ),
          const Divider(height: 1.0),
          SafeArea(
            top: false,
            child: Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTextComposer() {
    return IconTheme(
      data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: Row(
          children: [
            Text('>', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(width: 4),
            Flexible(
              child: TextField(
                controller: _controller,
                decoration: const InputDecoration(
                  border: InputBorder.none,
                ),
                onChanged: (text) {
                  setState(() {
                    _isComposing = text.isNotEmpty;
                  });
                },
                onSubmitted: _isComposing ? _handleSubmitted : null,
                focusNode: _focusNode,
              ),
            ),
            Container(
              margin: const EdgeInsets.symmetric(horizontal: 4.0),
              child: IconButton(
                icon: const Icon(Icons.send),
                onPressed: _isComposing
                    ? () => _handleSubmitted(_controller.text)
                    : null,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

چیزهای زیادی در این کد می گذرد، اما توضیح همه آن از محدوده این کد لبه خارج است. پیشنهاد می کنم کد را اجرا کنید و پس از بررسی مستندات مناسب، کد را اصلاح کنید.

$ cd example
$ flutter run

Duktape REPL در حال اجرا در یک برنامه لینوکس

Duktape REPL در حال اجرا در یک برنامه ویندوز

Duktape REPL در یک شبیه ساز iOS اجرا می شود

Duktape REPL در یک شبیه ساز اندروید اجرا می شود

8. تبریک می گویم

تبریک می گویم! شما با موفقیت یک افزونه مبتنی بر Flutter FFI برای Windows، macOS، Linux، Android و iOS ایجاد کردید!

پس از ایجاد یک افزونه، ممکن است بخواهید آن را به صورت آنلاین به اشتراک بگذارید تا دیگران بتوانند از آن استفاده کنند. می‌توانید مستندات کامل انتشار افزونه خود را در pub.dev در بسته‌های افزونه توسعه پیدا کنید.