Flutter eklentisinde FFI kullanma

1. Giriş

Dart'ın FFI (yabancı işlev arayüzü) özelliği, Flutter uygulamalarının C API'sini açığa çıkaran mevcut yerel kitaplıklardan yararlanmasına olanak tanır. Dart; Android, iOS, Windows, macOS ve Linux'ta FFI'yı destekler. Dart, web için JavaScript birlikte çalışabilirlik özelliğini destekler ancak bu konu codelab'de ele alınmamıştır.

Oluşturacaklarınız

Bu codelab'de, C kitaplığı kullanan bir mobil ve masaüstü eklentisi oluşturacaksınız. Bu API ile, eklentiden faydalanan basit bir örnek uygulama yazacaksınız. Eklentiniz ve uygulamanız:

  • C kitaplığı kaynak kodunu yeni Flutter eklentinize aktarma
  • Eklentiyi özelleştirerek Windows, macOS, Linux, Android ve iOS üzerinde derlenmesine izin verin
  • JavaScript REPL (okuma ve yazdırma döngüsünü okuma) için eklentiyi kullanan bir uygulama derleme

Duktape REPL, macOS uygulaması olarak çalışıyor

Neler öğreneceksiniz?

Bu codelab'de, hem masaüstü hem de mobil platformlarda FFI tabanlı Flutter eklentisi geliştirmek için gereken pratik bilgileri öğreneceksiniz. Bu bilgilere şunlar dahildir:

  • Dart FFI tabanlı Flutter eklentisi şablonu oluşturma
  • C kitaplığı için bağlama kodu oluşturmak amacıyla ffigen paketini kullanma
  • Android, Windows ve Linux için bir Flutter FFI eklentisi oluşturmak üzere CMake'i kullanma
  • iOS ve macOS için Flutter FFI eklentisi derlemek amacıyla CocoaPods kullanma

Gerekenler

  • Android geliştirme için Android Studio 4.1 veya sonraki sürümler
  • iOS ve macOS geliştirme için Xcode 13 veya sonraki sürümler
  • "C++ ile masaüstü geliştirme" ile Visual Studio 2022 veya Visual Studio Derleme Araçları 2022 Windows masaüstü geliştirmesi için iş yükü
  • Flutter SDK'sı
  • Geliştirme yapacağınız platformlar (ör. CMake, CocoaPods vb.) için gerekli tüm derleme araçları.
  • Geliştireceğiniz platformlar için LL sanal makine (LLVM). LLVM derleyici aracı paketi, ffigen tarafından C üst bilgi dosyasını ayrıştırarak Dart'ta gösterilen FFI bağlamasını oluşturmak amacıyla kullanılır.
  • Visual Studio Code gibi bir kod düzenleyici.

2. Başlarken

ffigen aracı, Flutter'a yeni eklenen bir özelliktir. Aşağıdaki komutu çalıştırarak Flutter yüklemenizin mevcut kararlı sürümü çalıştırdığından emin olabilirsiniz.

$ 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 çıkışının, kararlı kanalında olduğunuzu ve daha yeni kararlı Flutter sürümlerinin olmadığını belirttiğinden emin olun. Kararlı değilseniz veya daha yeni sürümler varsa Flutter aracınızı hızlandırmak için aşağıdaki iki komutu çalıştırın.

$ flutter channel stable
$ flutter upgrade

Bu codelab'deki kodu aşağıdaki cihazlardan birini kullanarak çalıştırabilirsiniz:

  • Geliştirme bilgisayarınız (eklentinizin ve örnek uygulamanızın masaüstü derlemeleri için)
  • Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android veya iOS cihaz
  • iOS simülatörü (Xcode araçlarının yüklenmesini gerektirir)
  • Android Emülatör (Android Studio'da kurulum gerektirir)

3. Eklenti şablonunu oluşturma

Flutter eklentisi geliştirmeye başlama

Flutter, kullanmaya başlamayı kolaylaştıran eklenti şablonları sunar. Eklenti şablonunu oluşturduğunuzda hangi dili kullanmak istediğinizi belirtebilirsiniz.

Eklenti şablonunu kullanarak projenizi oluşturmak için çalışma dizininizde aşağıdaki komutu çalıştırın:

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

--platforms parametresi, eklentinizin hangi platformları destekleyeceğini belirtir.

Oluşturulan projenin düzenini tree komutunu veya işletim sisteminizin dosya gezginini kullanarak inceleyebilirsiniz.

$ 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

Dizin yapısına göz atarak nelerin oluşturulduğu ve nerede bulunduğuyla ilgili fikir edinebilirsiniz. plugin_ffi şablonu, eklentiye ilişkin Dart kodunu lib altına, android, ios, linux, macos ve windows adlı platforma özel dizinlere ve en önemlisi, bir example dizinine yerleştirir.

Flutter'ı normal şekilde geliştirmede kullanılan bir geliştirici için bu yapı, üst düzeyde tanımlanmış yürütülebilir bir öğe olmadığından garip gelebilir. Bir eklentinin diğer Flutter projelerine eklenmesi gerekir, ancak eklenti kodunuzun çalıştığından emin olmak için example dizininde kodu hazırlarsınız.

Artık başlayabilirsiniz.

4. Örneği derleyip çalıştırma

Derleme sisteminin ve ön koşulların doğru şekilde yüklendiğinden ve desteklenen her platformda çalıştığından emin olmak amacıyla her hedef için oluşturulan örnek uygulamayı derleyip çalıştırın.

Windows

Windows'un desteklenen bir sürümünü kullandığınızdan emin olun. Bu codelab'in Windows 10 ve Windows 11'de çalıştığı bilinmektedir.

Uygulamayı kod düzenleyicinizin içinden veya komut satırından derleyebilirsiniz.

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=/

Aşağıdaki gibi çalışan bir uygulama penceresi görürsünüz:

Şablon olarak üretilen ve Windows uygulaması olarak çalışan FFI uygulaması

Linux

Linux'un desteklenen bir sürümünü kullandığınızdan emin olun. Bu codelab'de Ubuntu 22.04.1 kullanılır.

2. Adım'da listelenen tüm ön koşulları yükledikten sonra, aşağıdaki komutları bir terminalde çalıştırın:

$ 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=/

Aşağıdaki gibi çalışan bir uygulama penceresi görürsünüz:

Şablon olarak üretilen ve Linux uygulaması olarak çalıştırılan FFI uygulaması

Android

Android'de derleme için Windows, macOS veya Linux'u kullanabilirsiniz. Öncelikle, geliştirme bilgisayarınıza bağlı bir Android cihazınız olduğundan veya bir Android Emulator (AVD) örneği çalıştırdığınızdan emin olun. Aşağıdaki komutu çalıştırarak Flutter'ın Android cihaza veya emülatöre bağlanabildiğini onaylayın:

$ 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

Şablon tarafından oluşturulan ve Android emülatörde çalışan FFI uygulaması

macOS ve iOS

macOS ve iOS Flutter ile geliştirme için macOS bilgisayar kullanmanız gerekir.

macOS'te örnek uygulamayı çalıştırarak başlayın. Flutter'ın gördüğü cihazları tekrar onaylayın:

$ 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

Oluşturulan eklenti projesini kullanarak örnek uygulamayı çalıştırın:

$ cd ffigen_app/example
$ flutter run -d macos

Aşağıdaki gibi çalışan bir uygulama penceresi görürsünüz:

Şablon olarak üretilen ve Linux uygulaması olarak çalıştırılan FFI uygulaması

iOS için simülatörü veya gerçek bir donanım cihazını kullanabilirsiniz. Simülatörü kullanıyorsanız önce simülatörü çalıştırın. Artık flutter devices komutu simülatörü kullanılabilir cihazlarından biri olarak listeliyor.

$ 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

Simülasyon aracı başladıktan sonra şu komutu çalıştırın: flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

iOS simülatöründe çalışan, şablon tarafından oluşturulmuş FFI uygulaması

iOS simülatörü, macOS hedefine göre önceliklidir. Bu nedenle, -d parametresiyle cihaz belirtme adımını atlayabilirsiniz.

Tebrikler, beş farklı işletim sisteminde başarılı bir şekilde uygulama oluşturup çalıştırdınız. Ardından, yerel eklentiyi oluşturma ve FFI kullanarak Dart'tan arayüz oluşturma.

5. Windows, Linux ve Android'de Duktape'yi Kullanma

Bu codelab'de kullanacağınız C kitaplığı Duktape'tir. Duktape, yerleştirilebilir bir JavaScript motorudur. Odak noktası taşınabilirlik ve kompakt bir ayak izidir. Bu adımda, eklentiyi Duktape kitaplığını derleyecek, eklentinize bağlayacak ve Dart'ın FFI kullanarak erişecek şekilde yapılandıracaksınız.

Bu adım, entegrasyonu Windows, Linux ve Android'de çalışacak şekilde yapılandırır. iOS ve macOS entegrasyonu, derlenen kitaplığı nihai Flutter yürütülebilir dosyasına dahil etmek için (bu adımda açıklananların ötesinde) ek yapılandırma gerektirir. Gerekli ek yapılandırma, sonraki adımda ele alınmıştır.

Duktape Alınıyor

Öncelikle, duktape.org web sitesinden indirerek duktape kaynak kodunun bir kopyasını alın.

Windows'da Invoke-WebRequest ile Powerpoint'i kullanabilirsiniz:

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

Linux için wget iyi bir seçimdir.

$ 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]

Dosya bir tar.xz arşivi. Windows'da 7Zip araçlarını indirip aşağıdaki gibi kullanabilirsiniz.

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

Önce xz sıkıştırmasını arşivden çıkarmak, ikinci olarak da tar arşivini genişletmek için 7z'i iki kez çalıştırmanız gerekir.

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

Modern Linux ortamlarında tar, içerikleri aşağıdaki gibi tek adımda çıkarır.

$ 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 yükleme

ffigen öğesini kullanmak için ffigen tarafından C başlıklarını ayrıştırmak için kullanılan LLVM'yi yüklemeniz gerekir. Windows'da aşağıdaki komutu çalıştırın.

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

Windows makinenizde LLVM kurulumunu tamamlamak için sistem yollarınızı ikili program arama yolunuza C:\Program Files\LLVM\bin ekleyecek şekilde yapılandırın. Uygulamanın doğru yüklenip yüklenmediğini aşağıdaki adımları uygulayarak test edebilirsiniz.

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

Ubuntu için LLVM bağımlılığı aşağıdaki gibi yüklenebilir. Diğer Linux dağıtımları, LLVM ve Clang için benzer bağımlılıklara sahiptir.

$ 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) ...

Yukarıda olduğu gibi, Linux'ta LLVM yüklemenizi aşağıdaki şekilde test edebilirsiniz.

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

ffigen yapılandırılıyor

Oluşturulan üst düzey pubpsec.yaml olan şablonda ffigen paketinin eski sürümleri bulunuyor olabilir. Eklenti projesindeki Dart bağımlılıklarını güncellemek için aşağıdaki komutu çalıştırın:

$ flutter pub upgrade --major-versions

ffigen paketi güncel olduğuna göre şimdi ffigen ürününün bağlama dosyalarını oluşturmak için hangi dosyaları kullanacağını yapılandırın. Projenizin ffigen.yaml dosyasının içeriğini aşağıdakilerle eşleşecek şekilde değiştirin.

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

Bu yapılandırma, LLVM'ye iletilecek C başlık dosyasını, oluşturulacak çıkış dosyasını, dosyanın üst kısmına yerleştirilecek açıklamayı ve lint uyarısı eklemek için kullanılan bir önsöz bölümünü içerir.

Dosyanın sonunda daha fazla açıklama gerektiren bir yapılandırma öğesi var. ffigen sürümünün 11.0.0 sürümünden itibaren, başlık dosyaları ayrıştırılırken clang tarafından oluşturulan uyarılar veya hatalar varsa bağlama oluşturucu, varsayılan olarak bağlama oluşturmayacaktır.

Duktape başlık dosyaları, yazıldığı gibi, Duktape'nin işaretçilerinde boş değer türü tanımlayıcı eksikliği olduğundan macOS'te clang uyarısını tetikler. macOS ve iOS Duktape'nin tam olarak desteklenmesi için bu tür tanımlayıcıların Duktape kod tabanına eklenmesi gerekir. Bu arada, ignore-source-errors işaretini true olarak ayarlayarak bu uyarıları yoksayma kararı aldık.

Bir üretim uygulamasında, uygulamanızı göndermeden önce tüm derleyici uyarılarını ortadan kaldırmalısınız. Ancak Duktape için bunu yapmak, bu codelab'in kapsamı dışındadır.

Diğer anahtarlar ve değerler hakkında daha fazla bilgi için ffigen dokümanlarına bakın.

Belirli Duktape dosyalarını Duktape dağıtımından ffigen ürününün bu dosyaları bulmak için yapılandırıldığı konuma kopyalamanız gerekir.

$ 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/

Teknik olarak, ffigen için yalnızca duktape.h uygulaması üzerinden kopyalamanız gerekir, ancak CMake'i üçünün de gerekli olduğu kitaplığı oluşturmak için yapılandırmak üzeresiniz. Yeni bağlamayı oluşturmak için ffigen komutunu çalıştırın:

$ 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

Her işletim sisteminde farklı uyarılar görürsünüz. Duktape 2.7.0'ın Windows, Linux ve macOS'te clang ile derleme yaptığı bilindiğinden bunları şimdilik yoksayabilirsiniz.

CMake'i yapılandırma

CMake, bir derleme sistemi oluşturma sistemidir. Bu eklenti, oluşturulan Flutter ikili programına Duktape'yi dahil etmek için CMake'i kullanarak Android, Windows ve Linux'a yönelik derleme sistemi oluşturur. Şablon tarafından oluşturulan CMake yapılandırma dosyasını aşağıdaki gibi değiştirmeniz gerekir.

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 yapılandırması, kaynak dosyaları ekler ve daha da önemlisi, Windows'da oluşturulan kitaplık dosyasının varsayılan davranışını tüm C simgelerini varsayılan olarak dışa aktaracak şekilde değiştirir. Bu, Duktape'nin olduğu Unix tarzı kitaplıkları Windows dünyasına taşımaya yardımcı olan bir CMake çalışmasıdır.

lib/ffigen_app.dart içeriğini aşağıdakiyle değiştirin.

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;
}

Bu dosya, dinamik bağlantı kitaplığı dosyasını yüklemekten (Linux ve Android için .so, Windows için .dll) ve temel C koduna daha Dart deyimsel arayüz sunan bir sarmalayıcı sağlamaktan sorumludur.

Bu dosya ffi paketini doğrudan içe aktardığından, paketi dev_dependencies konumundan dependencies konumuna taşımanız gerekir. Bunu yapmanın kolay bir yolu aşağıdaki komutu çalıştırmaktır:

$ dart pub add ffi

Örnekteki main.dart içeriğini aşağıdakiyle değiştirin.

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)}';
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Şimdi aşağıdaki adımları uygulayarak örnek uygulamayı tekrar çalıştırabilirsiniz:

$ cd example
$ flutter run

Uygulamanın şu şekilde çalıştığını göreceksiniz:

Windows uygulamasında başlatılan Duktape&#39;yi gösterme

Windows uygulamasında Duktape JavaScript çıkışını gösterme

Bu iki ekran görüntüsü, JavaScript'i Çalıştır düğmesine basmadan önceki ve sonraki durumu göstermektedir. Bu görselde, Dart'tan JavaScript kodunun yürütülmesi ve sonucu ekranda gösterilmektedir.

Android

Android, çekirdek tabanlı, Linux işletim sistemidir ve masaüstü Linux dağıtımlarına kısmen benzer. CMake derleme sistemi, iki platform arasındaki farkların çoğunu gizleyebilir. Android'de uygulama derlemek ve çalıştırmak için Android emülatörünün çalıştığından (veya Android cihazın bağlı olduğundan) emin olun. Uygulamayı çalıştırın. Örnek:

$ cd example
$ flutter run -d emulator-5554

Şimdi Android'de çalışan örnek uygulamayı görüyor olmalısınız:

Android emülatöründe başlatılan Duktape gösteriliyor

Android emülatöründe Duktape JavaScript çıkışını gösterme

6. Duktape'yi macOS ve iOS'te kullanma

Şimdi, eklentinizi birbiriyle yakından ilişkili iki işletim sistemi olan macOS ve iOS'te çalıştırmanın zamanı geldi. macOS'le başlayın. CMake, macOS ve iOS'i desteklese de Linux ve iOS için yaptığınız önceki çalışmayı yeniden kullanamazsınız. Android, macOS ve iOS'te Flutter'ı kullanarak kitaplıkları içe aktarmak için CocoaPods kullanır.

Temizleme

Önceki adımda Android, Windows ve Linux için çalışan bir uygulama geliştirmiştiniz. Ancak orijinal şablonda kalmış ve şimdi temizlemeniz gereken birkaç dosya vardır. Bunları aşağıdaki gibi hemen kaldırabilirsiniz.

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

macOS

macOS platformunda Flutter, C ve C++ kodlarını içe aktarmak için CocoaPods kullanır. Yani bu paketin, CocoaPods derleme altyapısına entegre edilmesi gerekir. Önceki adımda CMake ile derlemek üzere yapılandırdığınız C kodunun yeniden kullanımını etkinleştirmek için macOS platform çalıştırıcısına tek bir yönlendirme dosyası eklemeniz gerekir.

macos/Classes/duktape.c

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

Bu dosya, önceki adımda ayarladığınız yerel kaynak kodundaki kaynak kodunu dahil etmek için C ön işlemcisinin gücünü kullanır. Bunun işleyiş şekli hakkında daha ayrıntılı bilgi için macos/ffigen_app.podspec adresini ziyaret edin.

Bu uygulama çalıştırıldığında artık Windows ve Linux'ta gördüğünüz şekilde aynı kalıp izlenir.

$ cd example
$ flutter run -d macos

macOS uygulamasında başlatılan Duktape&#39;yi gösterme

macOS uygulamasında Duktape JavaScript çıkışını gösterme

iOS

macOS kurulumuna benzer şekilde, iOS için de tek bir yönlendirme C dosyasının eklenmesi gerekir.

ios/Classes/duktape.c

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

Bu tek dosyayla eklentiniz artık iOS'te çalışacak şekilde yapılandırılmıştır. Her zamanki gibi çalıştırın.

$ flutter run -d iPhone

iOS simülatöründe başlatılan Duktape&#39;yi gösterme

iOS simülatöründe Duktape JavaScript çıkışını gösterme

Tebrikler! Yerel kodu beş platforma başarıyla entegre ettiniz. Burası bir kutlama alanı. Hatta bir sonraki adımda geliştireceğiniz daha işlevsel bir kullanıcı arayüzü bile olabilir.

7. Okuma Değerlendirmesi Yazdırma Döngüsü'nü uygulayın

Hızlı etkileşimli bir ortamda bir programlama diliyle etkileşim kurmak çok daha eğlencelidir. Böyle bir ortamın ilk uygulaması, LISP'nin Read Eval Print Döngüsü (REPL) olmuştur. Bu adımda Duktape ile benzer bir uygulama yapacaksınız.

İçerikleri üretime hazırlama

Duktape C kitaplığıyla etkileşimde bulunan mevcut kod, hiçbir şeyin ters gidemeyeceğini varsayar. Ayrıca, test sırasında Duktape dinamik bağlantı kitaplıkları yüklenemiyor. Bu entegrasyon üretimini hazır hale getirmek için lib/ffigen_app.dart üzerinde birkaç değişiklik yapmanız gerekir.

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;
}

Dinamik bağlantı kitaplığını yükleme kodu, eklentinin bir test çalıştırıcısında kullanıldığı durumu ele almak için genişletildi. Bu işlem, bu API'yi Flutter testi olarak kullanan bir entegrasyon testinin yazılmasını sağlar. JavaScript kod dizesini değerlendirecek kod, eksik veya yanlış kod gibi hata koşullarını doğru şekilde ele alacak şekilde genişletildi. Bu ek kod, dizelerin bayt dizileri olarak döndürüldüğü ve Dart dizelerine dönüştürülmesi gereken durumların nasıl ele alınacağını gösterir.

Paket ekleme

REPL oluştururken kullanıcı ile Duktape JavaScript motoru arasında bir etkileşim görüntüleyeceksiniz. Kullanıcı kod satırlarını girer ve Duktape hesaplamanın sonucuyla veya bir istisnayla yanıt verir. Yazmanız gereken ortak kod miktarını azaltmak için freezed kullanılır. Gösterilen içeriği temaya daha uygun hale getirmek için google_fonts simgesini ve durum yönetimi için de flutter_riverpod aracını kullanırsınız.

Gerekli bağımlılıkları örnek uygulamaya ekleyin:

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

Sonra, REPL etkileşimini kaydetmek için bir dosya oluşturun:

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;
}

Bu sınıfta, REPL'de üç türden biri olarak görüntülenen her çizginin şeklinin kolay ifade edilmesini sağlamak için freezed'nın birleştirme türü özelliği kullanılmaktadır. Bu noktada, oluşturulması gereken ek bir kod olduğu için kodunuzda büyük olasılıkla bir hata türü görüntülenmektedir. Şimdi bunu aşağıdaki gibi yapın.

$ flutter pub run build_runner build

Bu işlem, az önce yazdığınız kodun temel aldığı example/lib/duktape_message.freezed.dart dosyasını oluşturur.

Ardından, macOS yapılandırma dosyalarında birkaç değişiklik yaparak google_fonts ürününün yazı tipi verilerine ağ isteklerinde bulunmasını sağlamanız gerekir.

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'yi oluşturma

Hataları işlemek için entegrasyon katmanını güncellediğinize ve etkileşim için veri gösterimi oluşturduğunuza göre, artık örnek uygulamanın kullanıcı arayüzünü oluşturabilirsiniz.

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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Bu kodda çok şey yapılmaktadır ancak tüm bunları açıklamak bu codelab'in kapsamı dışındadır. Kodu çalıştırmanızı ve ilgili belgeleri inceledikten sonra kodda değişiklikler yapmanızı öneririz.

$ cd example
$ flutter run

Duktape REPL, Linux uygulamasında çalışıyor

Duktape REPL, Windows uygulamasında çalışıyor

Duktape REPL, iOS simülatöründe çalışıyor

Duktape REPL, Android emülatöründe çalışıyor

8. Tebrikler

Tebrikler! Windows, macOS, Linux, Android ve iOS için Flutter FFI tabanlı bir eklentiyi başarıyla oluşturdunuz.

Bir eklenti oluşturduktan sonra, diğer kullanıcıların da kullanabilmesi için eklentiyi online olarak paylaşmak isteyebilirsiniz. Eklentinizi pub.dev'de yayınlamayla ilgili dokümanların tamamını Eklenti paketleri geliştirme bölümünde bulabilirsiniz.