Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
b125b70
Add `Asset`
colemancda Mar 21, 2026
8eb4304
Add `AssetManager`
colemancda Mar 21, 2026
4395c5b
Add `Configuration`
colemancda Mar 21, 2026
8625bd1
Add `AndroidFileManagerError`
colemancda Mar 21, 2026
27f87b4
Add `StorageManager`
colemancda Mar 21, 2026
5bba823
Add stubs
colemancda Mar 21, 2026
9627522
Add `AssetDirectory`
colemancda Mar 21, 2026
9ee69d6
Add `AssetManager.openDirectory()`
colemancda Mar 21, 2026
0188f61
Add stubs
colemancda Mar 21, 2026
9f68dea
Update `AndroidFileManagerError`
colemancda Mar 21, 2026
6364963
Add `AssetDirectory.Sequence`
colemancda Mar 21, 2026
5eeafe4
Remove older `AndroidAssetManager`
colemancda Mar 21, 2026
bbf7301
Rename `AssetDirectory.next()`
colemancda Mar 21, 2026
9fa6129
Upgrade to Swift 6
colemancda Mar 21, 2026
a9949d4
Update `AndroidChoreographer` for Swift 6
colemancda Mar 21, 2026
872a381
Add stubs
colemancda Mar 21, 2026
dde23eb
Merge branch 'main' into feature/file-manager
colemancda Mar 21, 2026
930abdc
Add license headers
colemancda Mar 21, 2026
9fbe312
Update `Asset.open()`
colemancda Mar 21, 2026
e962252
Add `Asset.withRawSpan()`
colemancda Mar 21, 2026
c34fcc1
Update for Swift 6.2
colemancda Mar 21, 2026
94e963b
Optimize `Asset.read()`
colemancda Mar 21, 2026
cf9b17a
Rename `CAndroidNDK`
colemancda Mar 21, 2026
84bbfb9
Add `SocketDescriptor`
colemancda Mar 21, 2026
a9a5731
Add `SocketDescriptor.Event`
colemancda Mar 21, 2026
ed414bc
Add socket event constants
colemancda Mar 21, 2026
991a2ba
Update `Looper` for Swift 6
colemancda Mar 21, 2026
6903c3a
Update `AndroidMainActor` for Swift 6
colemancda Mar 21, 2026
9731c4a
Add stubs
colemancda Mar 21, 2026
8b6bffb
Add `AndroidLooperError`
colemancda Mar 21, 2026
cafc0da
Add `Looper.Events`
colemancda Mar 21, 2026
fa7a7bc
Add `Looper.PrepareOptions`
colemancda Mar 21, 2026
75ca1ac
Add `Looper.Executor`
colemancda Mar 21, 2026
81dcd63
Add `Duration` extensions
colemancda Mar 21, 2026
ffaea45
Add `OptionSet` extensions
colemancda Mar 21, 2026
acc5ad6
Add `Thread` extensions
colemancda Mar 21, 2026
e9bd08b
Add license headers
colemancda Mar 21, 2026
c38e9bc
Fix `SocketDescriptor.Event.Flags`
colemancda Mar 21, 2026
4105970
Add `system_eventfd()`
colemancda Mar 21, 2026
4fe9e9c
Add `SocketDescriptor` operations
colemancda Mar 21, 2026
9d89bab
Optimize `Asset.read()`
colemancda Mar 21, 2026
848072d
Add `Asset.read()` variants
colemancda Mar 21, 2026
9227937
Remove assertion that LogTag is empty
marcprux Mar 21, 2026
7abb089
Update AndroidContext to use SwiftJavaJNICore
marcprux Mar 21, 2026
b884fa2
Update AndroidContext
marcprux Mar 21, 2026
389bdb9
Add AndroidContext.setSharedContext() and update docs
marcprux Mar 21, 2026
e6cdfdb
Fix logging imports and update README.md
marcprux Mar 21, 2026
594d668
Merge branch 'main' into feature/pureswift
colemancda Mar 21, 2026
2aec8e2
Fix merge conflicts
colemancda Mar 21, 2026
3c68421
Add `AndroidContextError`
colemancda Mar 21, 2026
87135d9
Add license header
colemancda Mar 21, 2026
a58f198
Update `AndroidContext` for Swift 6.2
colemancda Mar 21, 2026
9c24bc0
Add `AndroidLooperError`
colemancda Mar 21, 2026
d7e048c
Remove `LockedState`
colemancda Mar 21, 2026
33c70b0
Add `Duration` extensions
colemancda Mar 21, 2026
bfb2197
Restore `LockedState`
colemancda Mar 21, 2026
6df09b5
Add `AndroidContextError.virtualMachine`
colemancda Mar 21, 2026
0e9322a
Updated unit tests
colemancda Mar 21, 2026
063c2a9
Fix `AndroidContext.applicationContext`
colemancda Mar 21, 2026
e2593da
Add license header
colemancda Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.2
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAndroidNative open source project
Expand Down
45 changes: 20 additions & 25 deletions Sources/AndroidChoreographer/AndroidChoreographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,48 @@
import Android
import CAndroidNDK
#endif
import AndroidLogging
import CoreFoundation

//let logger = Logger(subsystem: "swift.android.native", category: "AndroidChoreographer")

/// https://developer.android.com/ndk/reference/group/choreographer
@available(macOS, unavailable)
/// Choreographer coordinates the timing of frame rendering.
///
/// This is the C version of the android.view.Choreographer object in Java. If you do not use Choreographer to pace your render loop, you may render too quickly for the display, increasing latency between frame submission and presentation.
///
/// Input events are guaranteed to be processed before the frame callback is called, and will not be run concurrently. Input and sensor events should not be handled in the Choregrapher callback.
///
/// The frame callback is also the appropriate place to run any per-frame state update logic. For example, in a game, the frame callback should be responsible for updating things like physics, AI, game state, and rendering the frame. Input and sensors should be handled separately via callbacks registered with AInputQueue and ASensorManager.
///
/// [See Also](https://developer.android.com/ndk/reference/group/choreographer)
//@available(macOS, unavailable)
@available(iOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public final class AndroidChoreographer: @unchecked Sendable {
private let _choreographer: OpaquePointer
public struct AndroidChoreographer : @unchecked Sendable {

private let pointer: OpaquePointer

/// Get the AChoreographer instance for the main thread.
///
/// Must be initialized at startup time with `setupMainChoreographer()`
public private(set) static var main: AndroidChoreographer!

public nonisolated(unsafe) private(set) static var main: AndroidChoreographer!
/// Get the AChoreographer instance for the current thread.
///
/// This must be called on an ALooper thread.
public static var current: AndroidChoreographer {
#if !os(Android)
fatalError("only implemented for Android")
#else
AndroidChoreographer(choreographer: AChoreographer_getInstance())
#endif
AndroidChoreographer(pointer: AChoreographer_getInstance()!)
}

init(choreographer: OpaquePointer) {
self._choreographer = choreographer
private init(pointer: OpaquePointer) {
self.pointer = pointer
}

/// Add a callback to the Choreographer to invoke `_dispatch_main_queue_callback_4CF` on each frame to drain the main queue
public static func setupMainChoreographer() {
if Self.main == nil {
//logger.info("setupMainQueue")
Self.main = AndroidChoreographer.current
//enqueueMainChoreographer()
}
}

public func postFrameCallback(_ callback: @convention(c) (Int, UnsafeMutableRawPointer?) -> ()) {
#if !os(Android)
fatalError("only implemented for Android")
#else
AChoreographer_postFrameCallback(_choreographer, callback, nil)
#endif
public func postFrameCallback(_ callback: @convention(c)(Int, UnsafeMutableRawPointer?) -> ()) {
AChoreographer_postFrameCallback(pointer, callback, nil)
}
}
34 changes: 34 additions & 0 deletions Sources/AndroidChoreographer/Constants.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Constants.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 7/6/25.
//

#if !os(Android)
import CoreFoundation

typealias AVsyncId = Int64

typealias AChoreographer_frameCallback = @convention(c) (Int, UnsafeMutableRawPointer?) -> Void
typealias AChoreographer_frameCallback64 = @convention(c) (Int64, UnsafeMutableRawPointer?) -> Void
typealias AChoreographer_vsyncCallback = @convention(c) (UnsafePointer<OpaquePointer>?, UnsafeMutableRawPointer?) -> Void
typealias AChoreographer_refreshRateCallback = @convention(c) (Int64, UnsafeMutableRawPointer?) -> Void

var ALOOPER_PREPARE_ALLOW_NON_CALLBACKS: Int { stub() }

var ALOOPER_EVENT_INPUT: Int { stub() }
var ALOOPER_EVENT_OUTPUT: Int { stub() }
var ALOOPER_EVENT_ERROR: Int { stub() }
var ALOOPER_EVENT_HANGUP: Int { stub() }
var ALOOPER_EVENT_INVALID: Int { stub() }

var ALOOPER_POLL_WAKE: Int { stub() }
var ALOOPER_POLL_CALLBACK: Int { stub() }
var ALOOPER_POLL_TIMEOUT: Int { stub() }
var ALOOPER_POLL_ERROR: Int { stub() }

// renamed on Darwin
var kCFRunLoopDefaultMode: CFRunLoopMode { .defaultMode }

#endif
152 changes: 152 additions & 0 deletions Sources/AndroidChoreographer/Syscalls.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//
// Syscalls.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 7/6/25.
//


#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Gblic
#endif

import Dispatch

// MARK: - Dispatch

// https://github.com/apple-oss-distributions/libdispatch/blob/bd82a60ee6a73b4eca50af028b48643d51aaf1ea/src/queue.c#L8237
// https://forums.swift.org/t/main-dispatch-queue-in-linux-sdl-app/31708/3
@_silgen_name("_dispatch_main_queue_callback_4CF")
func _dispatch_main_queue_callback_4CF()

@_silgen_name("_dispatch_get_main_queue_port_4CF")
func _dispatch_get_main_queue_port_4CF() -> Int32

#if !os(Android)

func stub() -> Never {
fatalError("Not running on Android JVM")
}

// MARK: - Looper

func ALooper_forThread() -> OpaquePointer? { stub() }

func ALooper_prepare(_ opts: Int32) -> OpaquePointer? { stub() }

func ALooper_acquire(_ looper: OpaquePointer) { stub() }

func ALooper_release(_ looper: OpaquePointer) { stub() }

func ALooper_wake(_ looper: OpaquePointer) { stub() }

func ALooper_pollOnce(
_ timeoutMillis: Int32,
_ outFd: UnsafeMutablePointer<Int32>?,
_ outEvents: UnsafeMutablePointer<Int32>?,
_ outData: UnsafeMutablePointer<UnsafeMutableRawPointer?>?
) -> Int32 { stub() }

func ALooper_pollAll(
_ timeoutMillis: Int32,
_ outFd: UnsafeMutablePointer<Int32>?,
_ outEvents: UnsafeMutablePointer<Int32>?,
_ outData: UnsafeMutablePointer<UnsafeMutableRawPointer?>?
) -> Int32 { stub() }

public typealias ALooper_callbackFunc = @convention(c) (
Int32, Int32, UnsafeMutableRawPointer?
) -> Int32

func ALooper_addFd(
_ looper: OpaquePointer,
_ fd: Int32,
_ ident: Int32,
_ events: Int32,
_ callback: ALooper_callbackFunc?,
_ data: UnsafeMutableRawPointer?
) -> Int32 { stub() }

func ALooper_removeFd(
_ looper: OpaquePointer,
_ fd: Int32
) -> Int32 { stub() }

// MARK: - Choreographer

func AChoreographer_getInstance() -> OpaquePointer? { stub() }

func AChoreographer_postFrameCallback(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_frameCallback,
_ data: UnsafeMutableRawPointer?
) { stub() }

func AChoreographer_postFrameCallbackDelayed(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_frameCallback,
_ data: UnsafeMutableRawPointer?,
_ delayMillis: Int
) { stub() }

func AChoreographer_postFrameCallback64(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_frameCallback64,
_ data: UnsafeMutableRawPointer?
) { stub() }

func AChoreographer_postFrameCallbackDelayed64(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_frameCallback64,
_ data: UnsafeMutableRawPointer?,
_ delayMillis: UInt32
) { stub() }

func AChoreographer_postVsyncCallback(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_vsyncCallback,
_ data: UnsafeMutableRawPointer?
) { stub() }

func AChoreographer_registerRefreshRateCallback(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_refreshRateCallback,
_ data: UnsafeMutableRawPointer?
) { stub() }

func AChoreographer_unregisterRefreshRateCallback(
_ choreographer: OpaquePointer,
_ callback: @escaping AChoreographer_refreshRateCallback,
_ data: UnsafeMutableRawPointer?
) { stub() }

func AChoreographerFrameCallbackData_getFrameTimeNanos(
_ data: OpaquePointer
) -> Int64 { stub() }

func AChoreographerFrameCallbackData_getFrameTimelinesLength(
_ data: OpaquePointer
) -> Int { stub() }

func AChoreographerFrameCallbackData_getPreferredFrameTimelineIndex(
_ data: OpaquePointer
) -> Int { stub() }

func AChoreographerFrameCallbackData_getFrameTimelineVsyncId(
_ data: OpaquePointer,
_ index: Int
) -> AVsyncId { stub() }

func AChoreographerFrameCallbackData_getFrameTimelineExpectedPresentationTimeNanos(
_ data: OpaquePointer,
_ index: Int
) -> Int64 { stub() }

func AChoreographerFrameCallbackData_getFrameTimelineDeadlineNanos(
_ data: OpaquePointer,
_ index: Int
) -> Int64 { stub() }

#endif
48 changes: 28 additions & 20 deletions Sources/AndroidContext/AndroidContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ public class AndroidContext: @unchecked Sendable {
///
/// The default value of the factory will be the value of the `SWIFT_ANDROID_CONTEXT_FACTORY` environment variable,
/// and if unset, will fall back to `android.app.ActivityThread.currentApplication()Landroid/app/Application;`.
public static var contextFactory = getenv("SWIFT_ANDROID_CONTEXT_FACTORY").flatMap({ String(cString: $0) }) ?? "android.app.ActivityThread.currentApplication()Landroid/app/Application;"
public static var contextFactory: String {
getenv("SWIFT_ANDROID_CONTEXT_FACTORY").flatMap({ String(cString: $0) }) ?? "android.app.ActivityThread.currentApplication()Landroid/app/Application;"
}

/// A global pointer to the application context, in case the application environment wants to initialize it directly without going through the factory method.
public static var contextPointer: jobject? = nil
public nonisolated(unsafe) static var contextPointer: jobject? = nil

/// The underlying JNI object pointer for this context.
public let pointer: jobject
Expand Down Expand Up @@ -74,11 +76,11 @@ public class AndroidContext: @unchecked Sendable {
}

/// A manually provided shared context, set via `setSharedContext(_:env:)`.
private static var sharedContext: AndroidContext? = nil
private nonisolated(unsafe) static var sharedContext: AndroidContext? = nil

/// Returns the application context.
public static var application: AndroidContext {
get throws {
get throws(AndroidContextError) {
if let sharedContext = sharedContext {
return sharedContext
}
Expand All @@ -88,14 +90,24 @@ public class AndroidContext: @unchecked Sendable {

/// Obtain the global application context by checking whether the static `contextPointer` is set,
/// and if not, using the `contextFactory` string to reflectively look up the global context.
private static let applicationContext: Result<AndroidContext, Error> = Result(catching: {
let jvm: JavaVirtualMachine = try JavaVirtualMachine.shared()
let env: JNIEnvironment = try jvm.environment()
private static let applicationContext: Result<AndroidContext, AndroidContextError> = {
let jvm: JavaVirtualMachine
let env: JNIEnvironment
do {
jvm = try JavaVirtualMachine.shared()
env = try jvm.environment()
}
catch let error as JavaVirtualMachine.VMError {
return .failure(.virtualMachine(error))
}
catch {
fatalError("Non-JavaVirtualMachine.VMError error thrown")
}
let jni: JNINativeInterface = env.pointee!.pointee

// if we have provided a manual context jobject, then we just use that and skip trying to access the factory
if let contextPointer = contextPointer {
return AndroidContext(pointer: contextPointer, env: env)
return .success(AndroidContext(pointer: contextPointer, env: env))
}

// alternative fallback mechanism:
Expand All @@ -109,7 +121,7 @@ public class AndroidContext: @unchecked Sendable {
// get the second part of the contextFactory parameter: currentApplication()Landroid/app/Application;
let contextFunctionParts = contextRemainder.split(separator: "(")
if contextFunctionParts.count != 2 {
throw ContextError(errorDescription: "Invalid contextFactory signature: \(contextFactory)")
return .failure(.invalidSignature(contextFactory))
}

let contextMethod = "" + contextFunctionParts[0]
Expand All @@ -119,19 +131,19 @@ public class AndroidContext: @unchecked Sendable {
let jniClassName = contextType.split(separator: ".").joined(separator: "/")

guard let cls: jclass = jni.FindClass(env, jniClassName) else {
throw ContextError(errorDescription: "Unable to find class \(contextType)")
return .failure(.classNotFound(contextType))
}

guard let mth: jmethodID = jni.GetStaticMethodID(env, cls, contextMethod, contextSig) else {
throw ContextError(errorDescription: "Unable to find method \(contextMethod)")
return .failure(.methodNotFound(contextMethod))
}

guard let ctx: jobject = jni.CallStaticObjectMethodA(env, cls, mth, []) else {
throw ContextError(errorDescription: "Factory method \(contextMethod) returned null")
return .failure(.nullValueForMethod(contextMethod))
}

return AndroidContext(pointer: ctx, env: env)
})
return .success(AndroidContext(pointer: ctx, env: env))
}()

/// The `AndroidAssetManager` for this context
public var assetManager: AssetManager {
Expand All @@ -146,12 +158,12 @@ public class AndroidContext: @unchecked Sendable {
}

/// Returns the package name for the current context
public func getPackageName() throws -> String? {
public func getPackageName() throws(AndroidContextError) -> String? {
let jni: JNINativeInterface = env.pointee!.pointee

let contextClass: jclass = jni.GetObjectClass(env, pointer)!
guard let getPackageNameID: jmethodID = jni.GetMethodID(env, contextClass, "getPackageName", "()Ljava/lang/String;") else {
throw ContextError(errorDescription: "Unable to find getPackageName method")
throw AndroidContextError.methodNotFound("getPackageName")
}

guard let javaString: jobject = jni.CallObjectMethodA(env, pointer, getPackageNameID, []) else {
Expand All @@ -166,8 +178,4 @@ public class AndroidContext: @unchecked Sendable {
jni.ReleaseStringUTFChars(env, javaString, utf8Chars)
return result
}

struct ContextError: LocalizedError {
var errorDescription: String?
}
}
Loading
Loading