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.)
- The content in those containers must be corresponding native types, e.g., values of a
NSArray
must all beNSObject
s, while values of aCFArrayRef
must all beOpaquePtr
s.- 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.
- 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.
- 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 NSObject
s 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.
你好