狐狸的小小窝

我是小狐狸~欢迎来我的小窝!
Multi-OS Engine 就是炫

Working with native types: Advanced

After using MOE for several years we discovered some common problems when dealing with native types (Core Foundation Types & Cocoa Types). Here are some of our findings.

Container Types (NSArray, NSDictionary, CFArrayRef, etc.)

  1. The content in those containers must be corresponding native types, e.g., values of a NSArray must all be NSObjects, while values of a CFArrayRef must all be OpaquePtrs.
    1. NatJ does not automatically convert Java-only types (include primitive types) to compatible native types and vice versa when dealing with containers. When adding values to those containers you need to manually convert it to a corresponding native type. And when reading the value you also need to explicitly convert it to a Java type.
    2. If you try to store a Core Foundation type in a Cocoa container, you need to cast it using the Toll-Free Bridging which I will talk about later.
  2. Arrays and dictionaries can not have null as either key or value.

If you ever used the iOS KeyChain APIs you may did something similar as follows in Objective-C(simplified):

CFErrorRef error = NULL;

SecAccessControlRef access =
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                kSecAccessControlPrivateKeyUsage|kSecAccessControlUserPresence,
                                &error);

NSData* tag = [@"some tag" dataUsingEncoding:NSUTF8StringEncoding];

NSDictionary* attributes =
@{ (id)kSecClass:                   (id)kSecClassKey,
   (id)kSecAttrKeyType:             (id)kSecAttrKeyTypeECSECPrimeRandom,
   (id)kSecAttrKeySizeInBits:       @256,
   (id)kSecAttrTokenID:             (id)kSecAttrTokenIDSecureEnclave,
   (id)kSecPrivateKeyAttrs:
       @{ (id)kSecAttrIsPermanent:    @YES,
          (id)kSecAttrApplicationTag:  tag,
          (id)kSecAttrAccessControl:  (__bridge id)access
        },
   };

SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes,
                                             &error);

The corresponding Kotlin code will be:

val pError = PtrFactory.newOpaquePtrReference(CFErrorRef::class.java)

val access = Security.SecAccessControlCreateWithFlags(
    CoreFoundation.CFAllocatorGetDefault(),
    Security.kSecAttrAccessibleWhenUnlockedThisDeviceOnly(),
    SecAccessControlCreateFlags.PrivateKeyUsage or SecAccessControlCreateFlags.UserPresence,
    pError
)
val tag = NSString.stringWithString("some tag").dataUsingEncoding(NSUTF8StringEncoding)

// Helper function to convert a CF type to Cocoa type
fun OpaquePtr.toNSObject() = ObjCRuntime.cast(this, NSObject::class.java)

val privateKeyAttrs = NSMutableDictionary.dictionary<Any, Any>() as NSMutableDictionary<Any, Any>
privateKeyAttrs[Security.kSecAttrIsPermanent().toNSObject()] = NSNumber.numberWithBool(true) // Convert Java `boolean` to Cocoa type
privateKeyAttrs[Security.kSecAttrApplicationTag().toNSObject()] = tag
privateKeyAttrs[Security.kSecAttrAccessControl().toNSObject()] = access.toNSObject()

val attributes = NSMutableDictionary.dictionary<Any, Any>() as NSMutableDictionary<Any, Any>
attributes[Security.kSecClass().toNSObject()] = Security.kSecClassKey().toNSObject()
attributes[Security.kSecAttrKeyType().toNSObject()] = Security.kSecAttrKeyTypeECSECPrimeRandom().toNSObject()
attributes[Security.kSecAttrKeySizeInBits().toNSObject()] = NSNumber.numberWithInt(256) // Convert Java `int` to Cocoa type
attributes[Security.kSecAttrTokenID().toNSObject()] = Security.kSecAttrTokenIDSecureEnclave().toNSObject()
attributes[Security.kSecPrivateKeyAttrs().toNSObject()] = privateKeyAttrs

// Cast NSDictionary to CFDictionaryRef
val cfAttributes = ObjCRuntime.cast(attributes, CFDictionaryRef::class.java)

val privateKey = Security.SecKeyCreateRandomKey(cfAttributes, pError)

With the help of this library I created, the code can be simplified as:

val pError = PtrFactory.newOpaquePtrReference(CFErrorRef::class.java)

val access = Security.SecAccessControlCreateWithFlags(
    CoreFoundation.CFAllocatorGetDefault(),
    Security.kSecAttrAccessibleWhenUnlockedThisDeviceOnly(),
    SecAccessControlCreateFlags.PrivateKeyUsage or SecAccessControlCreateFlags.UserPresence,
    pError
)

val attributes = mapOf<Any,Any>(
    Security.kSecClass() to Security.kSecClassKey(),
    Security.kSecAttrKeyType() to Security.kSecAttrKeyTypeECSECPrimeRandom(),
    Security.kSecAttrKeySizeInBits() to 256,
    Security.kSecAttrTokenID() to Security.kSecAttrTokenIDSecureEnclave(),
    Security.kSecPrivateKeyAttrs() to mapOf<Any,Any>(
        Security.kSecAttrIsPermanent() to true,
        Security.kSecAttrApplicationTag() to "some tag".toNSData(),
        Security.kSecAttrAccessControl() to access
    )
).toNSDictionary()

val privateKey = Security.SecKeyCreateRandomKey(attributes.bridge(), pError)

This library also has a help function for creating NSArray from a Java Collection:

val nsArray = listOf(1, 2, 3, "some string", someCFRef, someNSType).toNSArray()

Toll-Free Bridging & Memory Management

iOS developers are most likely to be familiar with Cocoa Touch frameworks/APIs (does Foundation framework sound familiar to you?), which use the programming languages Objective-C or Swift.

However, you may also heard about another framework called Core Foundation. It is a pure C framework and provides access to lower level APIs and types.

For iOS development, most frameworks you will use are all designed based on the Foundation framework which means the types they use are all NSObjects so ARC manages the memory for you and the memory will be automatically released when the object is no longer used. MOE does some tricks so Java‘s GC also helps when you use those types on Java side.

However some low level functions (such as Security framework if you try to use TouchId) are available in only C functions. Hens they use Core Foundation types which does not have ARC and requires manual memory management. Sadly Java‘s GC does no help here (and may even create new issues which I will talk about later).

Ownership, CFRetain() and CFRelease()

Anyone has any C experience might still remember those good old days struggling with malloc() and free() while Core Foundation brings you CFRetain() and CFRelease().

Apple has a fantastic document that tells you how Ownership works in manual memory management and when you should use CFRetain()and CFRelease() respectively. I highly recommend you to read it first:

Link: Memory Management Programming Guide for Core Foundation – Ownership Policy

Basically if an Core Foundation object is owned by you, you need to call CFRelease() once you are done with it; if it’s NOT owned by you, in most cases, you need to CFRetain() it, use it, then CFRelease() it afterwards:

// Create a CFString, which is owned by you
CFStringRef urlStr = CFStringCreateWithCString(kCFAllocatorDefault,
    "https://www.noisyfox.io", kCFStringEncodingUTF8);

/* Do something with urlStr here. */

// Release it afterwards
CFRelease(urlStr);
// Get something from a dict, which is not owned, so a CFRetain() is required
CFStringRef title = (CFStringRef)CFRetain(CFDictionaryGetValue(dict, CFSTR("title")));

/* Do something with title here. */

// Release it afterwards
CFRelease(urlStr);

You can easily convert above code to Java since MOE bindings provide all necessary functions. And with a little help from my library, you could write something like these in Kotlin:

CFStringCreateWithCString(kCFAllocatorDefault(), "title", CFStringBuiltInEncodings.UTF8).use { key ->

    CFDictionaryGetValue(dict, key).cast<CFStringRef>().retain().use { title ->
        /* Do something with title here. */
    }
}
// CFRelease is automatically called when block returns

The action block of .use() is wrapped in a try block so the resource is guaranteed to be released even if Exception is raised in the block.

autoreleasepool for Core Foundation types

.use() mentioned above will create a lot of nested scopes if you have a lot of those Core Foundation objects. That makes the code unreadable. With the help of autoreleasepool you could rewrite the above code to something like this:

autoreleasepool {
    val key = CFStringCreateWithCString(kCFAllocatorDefault(), "title", CFStringBuiltInEncodings.UTF8).autorelease()
    val title = CFDictionaryGetValue(dict, key).cast<CFStringRef>().retain().autorelease()

    /* Do something with title here. */
}
// CFRelease is automatically called for each object that called .autorelease() when autoreleasepool block ends

NB: Make sure the autoreleasepool you called is imported from package io.noisyfox.moe.natj since NatJ has it’s own impl org.moe.natj.objc.ObjCRuntime#autoreleasepool(java.lang.Runnable) which is not a Kotlin inline function which doesn’t support certain features (such as non-local returns and reified type parameters).

Type Bridging

WIP

Misc

The library also contains some other help functions for converting between Java types and native types such as ByteArray <=> NSData. I won’t list all functions here since you could easily find them in source code.

The library – NatJ-Kotlin

This is a library I created that contains some helper functions I used in my work. It’s currently available in only source code form. I will create a jar release and publish it to jcenter ASAP.

The library is written in Kotlin but most of the functions can also be used in Java.

Discussion

Any idea of library improving are welcomed. If you found any mistake in my post, or have any other problem that is not covered, please not hesitate to share with us by leaving comments.

1 Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据