日落果

日落果的随手记

twitter
github

Kotlin Native Practical Guide -- Two Ways to Obtain iOS Device Model

Requirements & Attempts#

Today, while tinkering with Kotlin Multiplatform, I encountered a simple requirement: to get the model of an iOS device.

I searched the Apple official documentation and found UIKit.UIDevice.

Here is the description of the model property:

Possible examples of model strings are "iPhone" and "iPod touch".

Obviously, this does not meet the requirement of obtaining the specific model. So I continued to Google and found this answer on Stack Overflow. In the highly upvoted answer, most of them used utsname or sysctlbyname. In fact, the implementation of these two methods is the same, with one being simple and the other being complex.

Replicating with Kotlin Native#

Method 1 (Simple)#

First, let's look at the Objective-C implementation, which is very simple. Declare systemInfo and then call uname with the pointer to systemInfo. Finally, get the content of systemInfo.machine and convert it to an NSString.

#import <sys/utsname.h> // import it in your header or implementation file.

NSString* deviceName() {
  struct utsname systemInfo;
  uname(&systemInfo);
  return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
}

We can easily convert this to Kotlin Native. First, declare a memScoped because memory operations like alloc must be done within memScoped:

@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
    return memScoped {
        // todo
    }
}

Use alloc<T> to declare a variable of type utsname and then pass the pointer to uname:

@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
    return memScoped {
        val systemInfo = alloc<utsname>()
        uname(systemInfo.ptr)
    }
}

Next, we find that the type of systemInfo.machine is CArrayPointer<ByteVar>, which is equivalent to char *. So how do we convert it to a Kotlin String? Kotlin Native provides an extension function for this: fun CPointer<ByteVar>.toKString(): String. As the name suggests, it is used to convert a CPointer<ByteVar> to a Kotlin String. Since CArrayPointer<T> = CPointer<T>, we can directly use this extension function to convert char * to String. The final code is as follows:

@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
    return memScoped {
        val systemInfo = alloc<utsname>()
        uname(systemInfo.ptr)
        systemInfo.machine.toKString()
    }
}

Method 2 (Complicated)#

The second method corresponds to the following Objective-C implementation:

+ (NSString *)getModel {
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *model = malloc(size);
    sysctlbyname("hw.machine", model, &size, NULL, 0);
    NSString *deviceModel = [NSString stringWithCString:model encoding:NSUTF8StringEncoding];
    free(model);
    return deviceModel;
}

After a simple analysis, it is not difficult to find that after calling sysctlbyname, the size of char is obtained, and then another call to sysctlbyname gets the value of hw.machine. Finally, it is converted to an NSString using stringWithCString.

As usual, let's declare a memScoped:

@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
    return memScoped {
        // todo
    }
}

Following the previous approach, first implement the first call to sysctlbyname:

val sizePtr = alloc<size_tVar>()
sysctlbyname("hw.machine", null, sizePtr.ptr, null, 0u)

It's very simple, just like before. Next, we declare a fixed-length array with allocArray, with a length of sizePtr, corresponding to char *model = malloc(size):

val model = allocArray<ByteVar>(sizePtr.value.toInt())

Then, call sysctlbyname directly, refer to the method in Method 1, call sysctlbyname and convert model to a Kotlin String. The final code is as follows:

@OptIn(ExperimentalForeignApi::class)
fun getDeviceInfo(): String {
    return memScoped {
        val sizePtr = alloc<size_tVar>()
        sysctlbyname("hw.machine", null, sizePtr.ptr, null, 0u)
        val model = allocArray<ByteVar>(sizePtr.value.toInt())
        sysctlbyname("hw.machine", model, sizePtr.ptr, null, 0u)
        model.toKString()
    }
}

Some may ask, why don't we have free in our code? This is where memScoped comes in. Kotlin Native manages memory automatically and frees model itself, so we don't need to worry about it.

Testing#

I ran it on the Xcode iOS simulator and successfully obtained the correct result (the virtual machine is x86_64, and if it were a physical machine, it would display the model code):
image

Since I don't have an iOS device, I can't test it on a physical machine.

Complaints#

Kotlin Native still needs improvement. Kotlin Multiplatform is already stable, and I hope JetBrains pays more attention to interoperation.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.