This commit is contained in:
EthanShoeDev
2025-09-13 12:21:20 -04:00
parent 3582ecaa9e
commit 6c972c8f13
78 changed files with 4022 additions and 303 deletions

View File

@@ -1,67 +0,0 @@
name: 🐛 Bug report
description: Report a reproducible bug or regression in this library.
labels: [bug]
body:
- type: markdown
attributes:
value: |
# Bug report
👋 Hi!
**Please fill the following carefully before opening a new issue ❗**
*(Your issue may be closed if it doesn't provide the required pieces of information)*
- type: checkboxes
attributes:
label: Before submitting a new issue
description: Please perform simple checks first.
options:
- label: I tested using the latest version of the library, as the bug might be already fixed.
required: true
- label: I tested using a [supported version](https://github.com/reactwg/react-native-releases/blob/main/docs/support.md) of react native.
required: true
- label: I checked for possible duplicate issues, with possible answers.
required: true
- type: textarea
id: summary
attributes:
label: Bug summary
description: |
Provide a clear and concise description of what the bug is.
If needed, you can also provide other samples: error messages / stack traces, screenshots, gifs, etc.
validations:
required: true
- type: input
id: library-version
attributes:
label: Library version
description: What version of the library are you using?
placeholder: "x.x.x"
validations:
required: true
- type: textarea
id: react-native-info
attributes:
label: Environment info
description: Run `react-native info` in your terminal and paste the results here.
render: shell
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
You must provide a clear list of steps and code to reproduce the problem.
value: |
1. …
2. …
validations:
required: true
- type: input
id: reproducible-example
attributes:
label: Reproducible example repository
description: Please provide a link to a repository on GitHub with a reproducible example.
validations:
required: true

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request 💡
url: https://github.com/EthanShoeDev/fressh/discussions/new?category=ideas
about: If you have a feature request, please create a new discussion on GitHub.
- name: Discussions on GitHub 💬
url: https://github.com/EthanShoeDev/fressh/discussions
about: If this library works as promised but you need help, please ask questions there.

View File

@@ -1,36 +0,0 @@
name: Setup
description: Setup Node.js and install dependencies
runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Restore dependencies
id: yarn-cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
**/node_modules
.yarn/install-state.gz
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
restore-keys: |
${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
${{ runner.os }}-yarn-
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
shell: bash
- name: Cache dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
**/node_modules
.yarn/install-state.gz
key: ${{ steps.yarn-cache.outputs.cache-primary-key }}

View File

@@ -1,166 +0,0 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
types:
- checks_requested
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup
uses: ./.github/actions/setup
- name: Lint files
run: yarn lint
- name: Typecheck files
run: yarn typecheck
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup
uses: ./.github/actions/setup
- name: Run unit tests
run: yarn test --maxWorkers=2 --coverage
build-library:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup
uses: ./.github/actions/setup
- name: Build package
run: yarn prepare
build-android:
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-
- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
runs-on: macos-latest
env:
XCODE_VERSION: 16.3
TURBO_CACHE_DIR: .turbo/ios
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-
- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
run: |
cd example
bundle install
bundle exec pod repo update --verbose
bundle exec pod install --project-directory=ios
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

View File

@@ -76,7 +76,7 @@ android/keystores/debug.keystore
.turbo/
# generated by bob
lib/
/lib
# React Native Codegen
ios/generated
@@ -88,9 +88,9 @@ nitrogen/
# From uniffi-bindgen-react-native
# rust_modules/
*.a
./android/
./ios/
./cpp/
./src/
/android
/ios
/cpp
/src
*.xcframework
*.podspec

View File

@@ -32,15 +32,15 @@
"!**/.*"
],
"scripts": {
"ubrn:ios": "ubrn build ios --and-generate",
"ubrn:android": "ubrn build android --and-generate --release",
"build": "if [ \"$(uname)\" = \"Darwin\" ]; then npm run build:ios; else npm run build:android; fi",
"build:ios": "ubrn build ios --and-generate && bob build",
"build:android": "ubrn build android --and-generate --release && bob build",
"ubrn:web": "ubrn build web",
"ubrn:clean": "rm -rfv cpp/ android/CMakeLists.txt android/src/main/java android/*.cpp ios/ src/Native* src/index.*ts* src/generated/",
"test": "jest",
"typecheck": "tsc",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"x:prepare": "pnpm run ubrn:ios && bob build",
"release": "release-it --only-version"
},
"keywords": [

View File

@@ -0,0 +1,83 @@
# Created by https://www.toptal.com/developers/gitignore/api/rust,xcode,android
# Edit at https://www.toptal.com/developers/gitignore?templates=rust,xcode,android
### Android ###
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
### Android Patch ###
gen-external-apklibs
# Replacement of .externalNativeBuild directories introduced
# with Android Studio 3.5.
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
### Xcode ###
## User settings
xcuserdata/
## Xcode 8 and earlier
*.xcscmblueprint
*.xccheckout
### Xcode Patch ###
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
**/xcshareddata/WorkspaceSettings.xcsettings
# End of https://www.toptal.com/developers/gitignore/api/rust,xcode,android
# Swift Package Manager
.build/
# macOS Ignores
.DS_Store
# Android (cargo-ndk outputs; they end up in the Android source tree)
*.so

View File

@@ -0,0 +1,3 @@
# file options
--exclude apple/Sources/UniFFI/foobar.swift

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,27 @@
Copyright (c) 2024, Ian Wagner
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Ian Wagner nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,48 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let binaryTarget: Target = .binaryTarget(
name: "FoobarCoreRS",
// IMPORTANT: Swift packages importing this locally will not be able to
// import the rust core unless you use a relative path.
// This ONLY works for local development. For a larger scale usage example, see https://github.com/stadiamaps/ferrostar.
// When you release a public package, you will need to build a release XCFramework,
// upload it somewhere (usually with your release), and update Package.swift.
// This will probably be the subject of a future blog.
// Again, see Ferrostar for a more complex example, including more advanced GitHub actions.
path: "./rust/target/ios/libfoobar-rs.xcframework"
)
let package = Package(
name: "Foobar",
platforms: [
.iOS(.v16),
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Foobar",
targets: ["Foobar"]
),
],
targets: [
binaryTarget,
.target(
name: "Foobar",
dependencies: [.target(name: "UniFFI")],
path: "apple/Sources/Foobar"
),
.target(
name: "UniFFI",
dependencies: [.target(name: "FoobarCoreRS")],
path: "apple/Sources/UniFFI"
),
.testTarget(
name: "FoobarTests",
dependencies: ["Foobar"],
path: "apple/Tests/FoobarTests"
),
]
)

View File

@@ -0,0 +1,113 @@
# UniFFI Starter
This is a simple, mostly minimal, but more-complex-than-just-addition
demonstration of how to use [UniFFI](https://github.com/mozilla/uniffi-rs)
in a structure that works for MOST use cases.
The project has a Rust core, a Swift Package, and a Gradle project
in a structure that I've found to be fairly general.
The goal is for you to fork this as as starter to get past the boilerplate
and get started writing your cross-platform library.
## Motivating use case
As promised, it's more complex than addition, but it's intentionally
a bit ridiculous to keep things fun and demonstrate a wide range of uses
in a small project.
I say small, but note that the vast majority is boilerplate
Read on for a guide to the "good parts".
We're building a "safe" calculator that can do integer arithmetic.
(The usual demostrations of just addition are not complex enough to be very useful.)
The Rust core will provide addition and division operators.
The native code will show you how to supply your OWN operators written in Swift and Kotlin.
Because nobody knows where math is going next,
this OBVIOUSLY needs to be completely extensible with new operators ;)
By "safe" of course I mean that on the Rust side, everything returns a result,
which is translated into exceptions for iOS and Android.
At least you'll get a clean exception from Rust to let you know
about your integer overflows and division by zero!
Now if this is all starting to sound like every other over-engineered (read: ruined)
Java project, it definitely is, but I hope it's useful
in showing a wide range of examples in an easy-to-understand project.
This demonstration library also embraces the
functional core/imperative shell architecture.
Jokes about over-engineering aside,
while this is total overkill for a calculator app,
you really SHOULD strongly consider using this pattern
for any non-trivial real-world library.
Hope this helps!
## Quick start
### Rust
Open up the project in your favorite editor and poke around the Cargo workspace
under `rust/`!
#### Stuff to look at
* All of the code is in `foobar/src/lib.rs`, including several unit tests
to demonstrate the Rust API.
* Also check `Cargo.toml` and the overall workspace structure to see how a UniFFI project needs to be structured on the Rust side.
* Check out `rust-toolchain.toml`; **if you aren't using `rustup`, this is your checklist of toolchains to install!**
### iOS
Before opening up the Swift package in Xcode, you need to build the Rust core.
```shell
cd rust/
./build-ios.sh
```
This generates an XCFramework and generates Swift bindings to the Rust core.
Check the script if you're interested in the gritty details.
**You need to do this every time you make Rust changes that you want reflected in the Swift Package!**
#### Stuff to look at
* `Package.swift` documents the UniFFI setup (which is... special thanks to SPM quirks).
* `SafeCalculator` and `SafeMultiply` in `Sources/Foobar` contain the Swift-y calculator wrapper class and multiplication operator.
* The unit tests in `Tests/FoobarTests/SafeCalculatorTests.swift` demonstrate usage.
### Android
Android is pretty easy to get rolling, and Gradle will build everything for you
after you get a few things set up.
Most importantly, you need to install [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk).
```shell
cargo install cargo-ndk
```
If you've tried building the Rust library already and you have rustup,
the requisite targets will probably be installed automatically.
If not, follow the steps in the [`cargo-ndk` README](https://github.com/bbqsrc/cargo-ndk)
to install the required Android targets.
Just open up the `android` project in Android Studio and you're good to go.
It took forever to get the tooling right, but now that it's there, it just works.
Note that the app target is intentionally super barebones;
it's only there to illustrate how Gradle project dependencies work within a single workspace.
#### Stuff to look at
* The interesting stuff lives under the `SafeCalculator` and `SafeMultiply` classes.
* Also check out the tests for an example of usage.
* You can also check out the Android tests.
* The gradle files are mostly boilerplate, but there are a few things in there needed for building the Rust library. That took a while to figure out, and I currently believe this is the easiest approach.
## Learning more
Obviously, UniFFI's GitHub is probably the best place to learn more and ask questions.
If you're interested in following a slightly more serious dev log about using UniFFI
in a real library, check out the following:
* [Ferrostar on GitHub](https://github.com/stadiamaps/ferrostar) - a next gen navigation SDK I'm developing along with @Archdoog.
* Follow [Stadia Maps](https://stadiamaps.com/) on your favorite channel (scroll down to the bottom for social links and a newsletter). We occasionally post tech blogs like [this one](https://stadiamaps.com/news/ferrostar-building-a-cross-platform-navigation-sdk-in-rust-part-1/). Or [this one](https://stadiamaps.com/news/ferrostar-building-a-cross-platform-navigation-sdk-in-rust-part-2/)!

View File

@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,71 @@
plugins {
alias libs.plugins.androidApplication
alias libs.plugins.jetbrainsKotlinAndroid
alias libs.plugins.ktfmt
alias libs.plugins.compose.compiler
}
android {
namespace 'com.ianthetechie.foobar'
compileSdk 34
defaultConfig {
applicationId "com.ianthetechie.foobar"
minSdk 29
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.10'
}
}
dependencies {
implementation project(':core')
implementation libs.androidx.ktx
implementation libs.androidx.lifecycle.runtime.ktx
implementation libs.androidx.activity.compose
implementation platform(libs.androidx.compose.bom)
implementation libs.androidx.compose.ui
implementation libs.androidx.compose.ui.graphics
implementation libs.androidx.compose.ui.tooling
implementation libs.androidx.compose.material3
implementation libs.androidx.appcompat
implementation libs.material
testImplementation libs.junit
androidTestImplementation platform(libs.androidx.compose.bom)
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espresso
androidTestImplementation libs.androidx.compose.ui.test.junit4
debugImplementation libs.androidx.compose.ui.tooling
debugImplementation libs.androidx.compose.ui.test.manifest
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,22 @@
package com.ianthetechie.foobar
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.ianthetechie.foobar", appContext.packageName)
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Foobar"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.Foobar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,42 @@
package com.ianthetechie.foobar
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.ianthetechie.core.SafeCalculator
import com.ianthetechie.foobar.ui.theme.FoobarTheme
class MainActivity : ComponentActivity() {
private val calculator = SafeCalculator()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
FoobarTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting("🦀says 1 + 1 = ${calculator.add(1,1)}")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(text = "Hello $name!", modifier = modifier)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
FoobarTheme { Greeting("Android") }
}

View File

@@ -0,0 +1,11 @@
package com.ianthetechie.foobar.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,63 @@
package com.ianthetechie.foobar.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme =
darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80)
private val LightColorScheme =
lightColorScheme(
primary = Purple40, secondary = PurpleGrey40, tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun FoobarTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
}

View File

@@ -0,0 +1,35 @@
package com.ianthetechie.foobar.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Foobar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">Foobar</string>
<string name="title_activity_main">MainActivity</string>
</resources>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Foobar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,16 @@
package com.ianthetechie.foobar
import org.junit.Assert.*
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias libs.plugins.androidApplication apply false
alias libs.plugins.jetbrainsKotlinAndroid apply false
alias libs.plugins.androidLibrary apply false
alias libs.plugins.cargo.ndk apply false
alias libs.plugins.compose.compiler apply false
}

View File

@@ -0,0 +1,86 @@
plugins {
alias libs.plugins.androidLibrary
alias libs.plugins.jetbrainsKotlinAndroid
alias libs.plugins.ktfmt
// The magic plugin that automates the cargo integration
alias libs.plugins.cargo.ndk
}
android {
namespace 'com.ianthetechie.foobar.core'
compileSdk 34
ndkVersion "26.2.11394342"
defaultConfig {
minSdk 29
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation libs.androidx.ktx
implementation libs.androidx.appcompat
// Essential! Note that UniFFI dictates the minimum supported version, and this may change with new releases
// TODO: Migrate version to TOML (doesn't work). Likely related issue: https://github.com/gradle/gradle/issues/21267
//noinspection UseTomlInstead
implementation 'net.java.dev.jna:jna:5.15.0@aar'
testImplementation libs.junit
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espresso
}
cargoNdk {
module = "../rust" // Directory containing Cargo.toml
librariesNames = ["libfoobar.so"]
extraCargoBuildArguments = ["-p", "foobar"]
}
android.libraryVariants.all { variant ->
def bDir = layout.buildDirectory.dir("generated/source/uniffi/${variant.name}/java").get()
def generateBindings = tasks.register("generate${variant.name.capitalize()}UniFFIBindings", Exec) {
workingDir '../../rust'
commandLine 'cargo', 'run', '-p', 'uniffi-bindgen', 'generate', '--library', '../android/core/src/main/jniLibs/arm64-v8a/libfoobar.so', '--language', 'kotlin', '--out-dir', bDir
dependsOn "buildCargoNdk${variant.name.capitalize()}"
}
variant.javaCompileProvider.get().dependsOn(generateBindings)
// Some stuff here is broken, since Android Tests don't run after running gradle build,
// but do otherwise. Also CI is funky.
tasks.named("compile${variant.name.capitalize()}Kotlin").configure {
dependsOn generateBindings
}
tasks.named("connectedDebugAndroidTest").configure {
dependsOn generateBindings
}
def sourceSet = variant.sourceSets.find { it.name == variant.name }
sourceSet.java.srcDir layout.buildDirectory.file("generated/source/uniffi/${variant.name}/java")
// UniFFI tutorial notes that they made several attempts like this but were unsuccessful coming
// to a good solution for forcing the directory to be marked as generated (short of checking in
// project files, I suppose).
// idea.module.generatedSourceDirs += file("${buildDir}/generated/source/uniffi/${variant.name}/java/uniffi")
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,35 @@
/**
* FIXME: This file should move out of Android Tests ASAP. It only exists here because I haven't yet
* figured out how to build and link the platform-native binaries via JNI just yet and this works.
* See https://github.com/willir/cargo-ndk-android-gradle/issues/12.
*
* This solution is STUPIDLY INEFFICIENT and will probably contribute to global climate change since
* an Android emulator uses like two whole CPU cores when idling.
*/
package com.ianthetechie.foobar.core
import com.ianthetechie.core.SafeCalculator
import org.junit.Assert.*
import org.junit.Test
class SafeCalculatorTest {
private val calculator = SafeCalculator()
@Test
fun addition() {
val res = calculator.add(2, 2)
assertEquals(4, res.value)
val res2 = calculator.chainAdd(7)
assertEquals(11, res2.value)
}
@Test
fun multiplication() {
val res = calculator.mul(2, 4)
assertEquals(8, res.value)
val res2 = calculator.chainMul(3)
assertEquals(24, res2.value)
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,75 @@
package com.ianthetechie.core
import uniffi.foobar.Calculator
import uniffi.foobar.ComputationResult
import uniffi.foobar.safeAdditionOperator
class SafeCalculator {
// Functional core; imperative shell. This is purely internal state with an imperative API
// wrapper.
private var calc = Calculator()
private val addOp = safeAdditionOperator()
private val mulOp = SafeMultiply()
val lastValue: ComputationResult?
get() = calc.lastResult()
/**
* Adds two numbers together and returns the result.
*
* Throws a ComputationException if the result overflows.
*/
fun add(lhs: Long, rhs: Long): ComputationResult {
calc = calc.calculate(addOp, lhs, rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!!
}
/**
* Chains addition using the previous computation result.
*
* Throws a ComputationException if the result overflows or there is no previous state for this
* calculator.
*/
fun chainAdd(rhs: Long): ComputationResult {
calc = calc.calculateMore(addOp, rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!!
}
/**
* Multiplies two numbers together and returns the result.
*
* Throws a ComputationException if the result overflows.
*/
fun mul(lhs: Long, rhs: Long): ComputationResult {
calc = calc.calculate(mulOp, lhs, rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!!
}
/**
* Chains multiplication using the previous computation result.
*
* Throws a ComputationException if the result overflows or there is no previous state for this
* calculator.
*/
fun chainMul(rhs: Long): ComputationResult {
calc = calc.calculateMore(mulOp, rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!!
}
}

View File

@@ -0,0 +1,14 @@
package com.ianthetechie.core
import uniffi.foobar.BinaryOperator
import uniffi.foobar.ComputationException
class SafeMultiply : BinaryOperator {
override fun perform(lhs: Long, rhs: Long): Long {
try {
return Math.multiplyExact(lhs, rhs)
} catch (e: ArithmeticException) {
throw ComputationException.Overflow()
}
}
}

View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@@ -0,0 +1,43 @@
[versions]
agp = "8.7.2"
kotlin = "2.0.20"
cargo-ndk = "0.3.4"
ktfmt = "0.20.1"
androidx-lifecycle = "2.8.7"
ktx = "1.13.1"
androidx-appcompat = "1.7.0"
androidx-activity-compose = "1.9.2"
compose = "2024.10.01"
junit = "4.13.2"
junitVersion = "1.2.1"
junitCompose = "1.7.5"
espressoCore = "3.6.1"
material = "1.12.0"
[libraries]
# AndroidX
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
# Jetpack Compose
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
# Testing
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "junitCompose" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
cargo-ndk = { id = "com.github.willir.rust.cargo-ndk-android", version.ref = "cargo-ndk" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }

View File

@@ -0,0 +1,6 @@
#Wed Feb 07 02:01:23 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Foobar"
include ':app'
include ':core'

View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,61 @@
import UniFFI
public class SafeCalculator {
// Functional core; imperative shell. This is purely internal state with an imperative API wrapper.
private var calc = UniFFI.Calculator()
private let addOp = safeAdditionOperator()
private let mulOp = SafeMultiply()
var lastValue: UniFFI.ComputationResult? {
return calc.lastResult()
}
/// Adds two numbers together and returns the result.
///
/// Throws if the result overflows
public func add(lhs: Int64, rhs: Int64) throws -> ComputationResult {
calc = try calc.calculate(op: addOp, lhs: lhs, rhs: rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!
}
/// Chains addition using the previous computation result.
///
/// Throws if the result overflows or there is no previous state for this calculator.
public func chainAdd(rhs: Int64) throws -> ComputationResult {
calc = try calc.calculateMore(op: addOp, rhs: rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!
}
/// Multiplies two numbers together and returns the result.
///
/// Throws if the result overflows
public func mul(lhs: Int64, rhs: Int64) throws -> ComputationResult {
calc = try calc.calculate(op: mulOp, lhs: lhs, rhs: rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!
}
/// Chains multiplication using the previous computation result.
///
/// Throws if the result overflows or there is no previous state for this calculator.
public func chainMul(rhs: Int64) throws -> ComputationResult {
calc = try calc.calculateMore(op: mulOp, rhs: rhs)
// Note that it is not possible for lastResult to be anything but
// a computed value at this point, so we can expose a nicer
// interface to Swift consumers of the low-level library.
return calc.lastResult()!
}
}

View File

@@ -0,0 +1,14 @@
import Foundation
import UniFFI
public class SafeMultiply: UniFFI.BinaryOperator {
public func perform(lhs: Int64, rhs: Int64) throws -> Int64 {
let (res, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
throw UniFFI.ComputationError.Overflow
}
return res
}
}

View File

@@ -0,0 +1,22 @@
@testable import Foobar
import XCTest
final class MyLibraryTests: XCTestCase {
let calculator = SafeCalculator()
func testAddition() throws {
let res = try calculator.add(lhs: 2, rhs: 2)
XCTAssertEqual(res.value, 4)
let res2 = try calculator.chainAdd(rhs: 7)
XCTAssertEqual(res2.value, 11)
}
func testMultiplication() throws {
let res = try calculator.mul(lhs: 2, rhs: 4)
XCTAssertEqual(res.value, 8)
let res2 = try calculator.chainMul(rhs: 3)
XCTAssertEqual(res2.value, 24)
}
}

View File

@@ -0,0 +1,565 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anyhow"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "basic-toml"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
dependencies = [
"serde",
]
[[package]]
name = "bytes"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "clap"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "foobar"
version = "0.1.0"
dependencies = [
"thiserror",
"uniffi",
]
[[package]]
name = "fs-err"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "goblin"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rinja"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5"
dependencies = [
"itoa",
"rinja_derive",
]
[[package]]
name = "rinja_derive"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b"
dependencies = [
"basic-toml",
"memchr",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"rinja_parser",
"rustc-hash",
"serde",
"syn",
]
[[package]]
name = "rinja_parser"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
dependencies = [
"memchr",
"nom",
"serde",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "scroll"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
]
[[package]]
name = "thiserror"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "unicase"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "uniffi"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe34585ac0275accf6c284d0080cc2840f3898c551cda869ec291b5a4218712c"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"clap",
"uniffi_bindgen",
"uniffi_build",
"uniffi_core",
"uniffi_macros",
]
[[package]]
name = "uniffi-bindgen"
version = "0.1.0"
dependencies = [
"uniffi",
]
[[package]]
name = "uniffi-bindgen-swift"
version = "0.1.0"
dependencies = [
"uniffi",
]
[[package]]
name = "uniffi_bindgen"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a792af1424cc8b3c43b44c1a6cb7935ed1fbe5584a74f70e8bab9799740266d"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"fs-err",
"glob",
"goblin",
"heck",
"once_cell",
"paste",
"rinja",
"serde",
"textwrap",
"toml",
"uniffi_meta",
"uniffi_udl",
]
[[package]]
name = "uniffi_build"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c4138211f2ae951018fcce6a978e1fcd1a47c3fd0bc0d5472a520520060db1"
dependencies = [
"anyhow",
"camino",
"uniffi_bindgen",
]
[[package]]
name = "uniffi_core"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c18baace68a52666d33d12d73ca335ecf27a302202cefb53b1f974512bb72417"
dependencies = [
"anyhow",
"bytes",
"once_cell",
"static_assertions",
]
[[package]]
name = "uniffi_internal_macros"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9902d4ed16c65e6c0222241024dd0bfeed07ea3deb7c470eb175e5f5ef406cd"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "uniffi_macros"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d82c82ef945c51082d8763635334b994e63e77650f09d0fae6d28dd08b1de83"
dependencies = [
"camino",
"fs-err",
"once_cell",
"proc-macro2",
"quote",
"serde",
"syn",
"toml",
"uniffi_meta",
]
[[package]]
name = "uniffi_meta"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d6027b971c2aa86350dd180aee9819729c7b99bacd381534511ff29d2c09cea"
dependencies = [
"anyhow",
"siphasher",
"uniffi_internal_macros",
]
[[package]]
name = "uniffi_udl"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52300b7a4ab02dc159a038a13d5bfe27aefbad300d91b0b501b3dda094c1e0a2"
dependencies = [
"anyhow",
"textwrap",
"uniffi_meta",
"weedle2",
]
[[package]]
name = "weedle2"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
dependencies = [
"nom",
]

View File

@@ -0,0 +1,11 @@
[workspace]
members = [
"foobar",
"uniffi-bindgen",
"uniffi-bindgen-swift",
]
resolver = "2"
[workspace.dependencies]
uniffi = "0.29"

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env zsh
set -e
set -u
# NOTE: You MUST run this every time you make changes to the core. Unfortunately, calling this from Xcode directly
# does not work so well.
# In release mode, we create a ZIP archive of the xcframework and update Package.swift with the computed checksum.
# This is only needed when cutting a new release, not for local development.
release=false
for arg in "$@"
do
case $arg in
--release)
release=true
shift # Remove --release from processing
;;
*)
shift # Ignore other argument from processing
;;
esac
done
# Potential optimizations for the future:
#
# * Only build one simulator arch for local development (we build both since many still use Intel Macs)
# * Option to do debug builds instead for local development
fat_simulator_lib_dir="target/ios-simulator-fat/release"
generate_ffi() {
echo "Generating framework module mapping and FFI bindings"
# NOTE: Convention requires the modulemap be named module.modulemap
cargo run -p uniffi-bindgen-swift -- target/aarch64-apple-ios/release/lib$1.a target/uniffi-xcframework-staging --swift-sources --headers --modulemap --module-name $1FFI --modulemap-filename module.modulemap
mkdir -p ../apple/Sources/UniFFI/
mv target/uniffi-xcframework-staging/*.swift ../apple/Sources/UniFFI/
mv target/uniffi-xcframework-staging/module.modulemap target/uniffi-xcframework-staging/module.modulemap
}
create_fat_simulator_lib() {
echo "Creating a fat library for x86_64 and aarch64 simulators"
mkdir -p $fat_simulator_lib_dir
lipo -create target/x86_64-apple-ios/release/lib$1.a target/aarch64-apple-ios-sim/release/lib$1.a -output $fat_simulator_lib_dir/lib$1.a
}
build_xcframework() {
# Builds an XCFramework
echo "Generating XCFramework"
rm -rf target/ios # Delete the output folder so we can regenerate it
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/lib$1.a -headers target/uniffi-xcframework-staging \
-library target/ios-simulator-fat/release/lib$1.a -headers target/uniffi-xcframework-staging \
-output target/ios/lib$1-rs.xcframework
if $release; then
echo "Building xcframework archive"
ditto -c -k --sequesterRsrc --keepParent target/ios/lib$1-rs.xcframework target/ios/lib$1-rs.xcframework.zip
checksum=$(swift package compute-checksum target/ios/lib$1-rs.xcframework.zip)
version=$(cargo metadata --format-version 1 | jq -r --arg pkg_name "$1" '.packages[] | select(.name==$pkg_name) .version')
sed -i "" -E "s/(let releaseTag = \")[^\"]+(\")/\1$version\2/g" ../Package.swift
sed -i "" -E "s/(let releaseChecksum = \")[^\"]+(\")/\1$checksum\2/g" ../Package.swift
fi
}
basename=foobar
cargo build -p $basename --lib --release --target x86_64-apple-ios
cargo build -p $basename --lib --release --target aarch64-apple-ios-sim
cargo build -p $basename --lib --release --target aarch64-apple-ios
generate_ffi $basename
create_fat_simulator_lib $basename
build_xcframework $basename

View File

@@ -0,0 +1,16 @@
[package]
name = "foobar"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
uniffi.workspace = true
thiserror = "1.0.64"
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
[lib]
crate-type = ["cdylib", "staticlib", "lib"]

View File

@@ -0,0 +1,212 @@
use std::sync::Arc;
// You must call this once
uniffi::setup_scaffolding!();
// What follows is an intentionally ridiculous whirlwind tour of how you'd expose a complex API to UniFFI.
#[derive(Debug, PartialEq, uniffi::Enum, Default)]
pub enum ComputationState {
/// Initial state with no value computed
#[default]
Init,
Computed {
result: ComputationResult,
},
}
#[derive(Copy, Clone, Debug, PartialEq, uniffi::Record)]
pub struct ComputationResult {
pub value: i64,
}
#[derive(Debug, PartialEq, thiserror::Error, uniffi::Error)]
pub enum ComputationError {
#[error("Division by zero is not allowed.")]
DivisionByZero,
#[error("Result overflowed the numeric type bounds.")]
Overflow,
#[error("There is no existing computation state, so you cannot perform this operation.")]
IllegalComputationWithInitState,
}
/// A binary operator that performs some mathematical operation with two numbers.
#[uniffi::export(with_foreign)]
pub trait BinaryOperator: Send + Sync {
fn perform(&self, lhs: i64, rhs: i64) -> Result<i64, ComputationError>;
}
/// A somewhat silly demonstration of functional core/imperative shell in the form of a calculator with arbitrary operators.
///
/// Operations return a new calculator with updated internal state reflecting the computation.
#[derive(PartialEq, Debug, Default, uniffi::Object)]
pub struct Calculator {
state: ComputationState,
}
#[uniffi::export]
impl Calculator {
#[uniffi::constructor]
pub fn new() -> Self {
Self::default()
}
pub fn last_result(&self) -> Option<ComputationResult> {
match self.state {
ComputationState::Init => None,
ComputationState::Computed { result } => Some(result),
}
}
/// Performs a calculation using the supplied binary operator and operands.
pub fn calculate(
&self,
op: Arc<dyn BinaryOperator>,
lhs: i64,
rhs: i64,
) -> Result<Calculator, ComputationError> {
let value = op.perform(lhs, rhs)?;
Ok(Calculator {
state: ComputationState::Computed {
result: ComputationResult { value },
},
})
}
/// Performs a calculation using the supplied binary operator, the last computation result, and the supplied operand.
///
/// The supplied operand will be the right-hand side in the mathematical operation.
pub fn calculate_more(
&self,
op: Arc<dyn BinaryOperator>,
rhs: i64,
) -> Result<Calculator, ComputationError> {
let ComputationState::Computed { result } = &self.state else {
return Err(ComputationError::IllegalComputationWithInitState);
};
let value = op.perform(result.value, rhs)?;
Ok(Calculator {
state: ComputationState::Computed {
result: ComputationResult { value },
},
})
}
}
#[derive(uniffi::Object)]
struct SafeAddition {}
// Makes it easy to construct from foreign code
#[uniffi::export]
impl SafeAddition {
#[uniffi::constructor]
fn new() -> Self {
SafeAddition {}
}
}
#[uniffi::export]
impl BinaryOperator for SafeAddition {
fn perform(&self, lhs: i64, rhs: i64) -> Result<i64, ComputationError> {
lhs.checked_add(rhs).ok_or(ComputationError::Overflow)
}
}
#[derive(uniffi::Object)]
struct SafeDivision {}
// Makes it easy to construct from foreign code
#[uniffi::export]
impl SafeDivision {
#[uniffi::constructor]
fn new() -> Self {
SafeDivision {}
}
}
#[uniffi::export]
impl BinaryOperator for SafeDivision {
fn perform(&self, lhs: i64, rhs: i64) -> Result<i64, ComputationError> {
if rhs == 0 {
Err(ComputationError::DivisionByZero)
} else {
lhs.checked_div(rhs).ok_or(ComputationError::Overflow)
}
}
}
// Helpers that only exist because the concrete objects above DO NOT have the requisite protocol conformances
// stated in the glue code. It's easy to extend classes in Swift, but you can't just declare a conformance in Kotlin.
// So, to keep things easy, we just do this as a compromise.
#[uniffi::export]
fn safe_addition_operator() -> Arc<dyn BinaryOperator> {
Arc::new(SafeAddition::new())
}
#[uniffi::export]
fn safe_division_operator() -> Arc<dyn BinaryOperator> {
Arc::new(SafeDivision::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addition() {
let calc = Calculator::new();
let op = Arc::new(SafeAddition {});
let calc = calc
.calculate(op.clone(), 2, 2)
.expect("Something went wrong");
assert_eq!(calc.last_result().unwrap().value, 4);
assert_eq!(
calc.calculate_more(op.clone(), i64::MAX),
Err(ComputationError::Overflow)
);
assert_eq!(
calc.calculate_more(op, 8)
.unwrap()
.last_result()
.unwrap()
.value,
12
);
}
#[test]
fn division() {
let calc = Calculator::new();
let op = Arc::new(SafeDivision {});
let calc = calc
.calculate(op.clone(), 2, 2)
.expect("Something went wrong");
assert_eq!(calc.last_result().unwrap().value, 1);
assert_eq!(
calc.calculate_more(op.clone(), 0),
Err(ComputationError::DivisionByZero)
);
assert_eq!(
calc.calculate(op, i64::MIN, -1),
Err(ComputationError::Overflow)
);
}
#[test]
fn compute_more_from_init_state() {
let calc = Calculator::new();
let op = Arc::new(SafeAddition {});
assert_eq!(
calc.calculate_more(op, 1),
Err(ComputationError::IllegalComputationWithInitState)
);
}
}

View File

@@ -0,0 +1,28 @@
# See https://rust-lang.github.io/rustup/overrides.html for details on how this file works
# and how you can override the choices made herein.
#
# Note that we UniFFI also sets their own toolchain (see https://github.com/mozilla/uniffi-rs/blob/main/rust-toolchain.toml).
# We will attempt to track stable unless we find that this breaks something.
# The iOS targets are easy and well-known, but the Android targets take a bit more work to deduce.
# We have drawn our list from https://github.com/mozilla/rust-android-gradle.
[toolchain]
channel = "stable"
targets = [
# iOS
"aarch64-apple-ios",
"x86_64-apple-ios",
"aarch64-apple-ios-sim",
# Android
"armv7-linux-androideabi",
"i686-linux-android",
"aarch64-linux-android",
"x86_64-linux-android",
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-gnu",
"x86_64-pc-windows-msvc",
]
components = ["clippy", "rustfmt"]

View File

@@ -0,0 +1,13 @@
[package]
name = "uniffi-bindgen-swift"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
uniffi = { workspace = true, features = ["cli"] }
[[bin]]
name = "uniffi-bindgen-swift"
path = "src/main.rs"

View File

@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_swift();
}

View File

@@ -0,0 +1,14 @@
[package]
name = "uniffi-bindgen"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
uniffi = { workspace = true, features = ["cli"] }
[[bin]]
name = "uniffi-bindgen"
path = "src/main.rs"

View File

@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}

View File

@@ -1,23 +1,11 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [".nvmrc", ".yarnrc.yml"],
"globalEnv": ["NODE_ENV"],
"tasks": {
"build:android": {
"env": ["ANDROID_HOME", "ORG_GRADLE_PROJECT_newArchEnabled"],
"inputs": [
"package.json",
"android",
"!android/build",
"src/*.ts",
"src/*.tsx"
],
"outputs": []
"build": {
"outputs": ["lib/**", "android/**", "ios/**", "cpp/**", "src/**"]
},
"build:ios": {
"env": ["RCT_USE_RN_DEP", "RCT_USE_PREBUILT_RNCORE"],
"inputs": ["package.json", "*.podspec", "ios", "src/*.ts", "src/*.tsx"],
"outputs": []
"typecheck": {
"dependsOn": ["build"]
}
},
"extends": ["//"]

View File

@@ -3,5 +3,5 @@
# branch: jhugman/bump-uniffi-to-0.29
# manifestPath: rust/foobar/Cargo.toml
rust:
directory: ./rust_modules/uniffi-starter
manifestPath: rust/foobar/Cargo.toml
directory: rust_modules/uniffi-starter/rust
manifestPath: foobar/Cargo.toml