blob: 4c5e01084e045d5c37d8f0011eef5ed613a6ba03 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show utf8;
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:ui' show Display, FlutterView;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import '../foundation/_features.dart';
import '_window.dart';
import '_window_positioner.dart';
import 'binding.dart';
// Do not import this file in production applications or packages published
// to pub.dev. Flutter will make breaking changes to this file, even in patch
// versions.
//
// All APIs in this file must be private or must:
//
// 1. Have the `@internal` attribute.
// 2. Throw an `UnsupportedError` if `isWindowingEnabled`
// is `false`.
//
// See: https://github.com/flutter/flutter/issues/30701.
const String _kWindowingDisabledErrorMessage = '''
Windowing APIs are not enabled.
Windowing APIs are currently experimental. Do not use windowing APIs in
production applications or plugins published to pub.dev.
To try experimental windowing APIs:
1. Switch to Flutter's main release channel.
2. Turn on the windowing feature flag.
See: https://github.com/flutter/flutter/issues/30701.
''';
/// [WindowingOwner] implementation for macOS.
///
/// If [Platform.isMacOS] is false, then the constructor will throw an
/// [UnsupportedError].
///
/// {@macro flutter.widgets.windowing.experimental}
///
/// See also:
///
/// * [WindowingOwner], the abstract class that manages native windows.
class WindowingOwnerMacOS extends WindowingOwner {
/// Creates a new [WindowingOwnerMacOS] instance.
///
/// {@macro flutter.widgets.windowing.experimental}
///
/// See also:
///
/// * [WindowingOwner], the abstract class that manages native windows.
@internal
WindowingOwnerMacOS() {
if (!isWindowingEnabled) {
throw UnsupportedError(_kWindowingDisabledErrorMessage);
}
if (!Platform.isMacOS) {
throw UnsupportedError('Only available on the macOS platform');
}
assert(
WidgetsBinding.instance.platformDispatcher.engineId != null,
'WindowingOwnerMacOS must be created after the engine has been initialized.',
);
}
@override
RegularWindowController createRegularWindowController({
required RegularWindowControllerDelegate delegate,
Size? preferredSize,
BoxConstraints? preferredConstraints,
String? title,
}) {
final res = RegularWindowControllerMacOS(
owner: this,
delegate: delegate,
preferredSize: preferredSize,
title: title,
);
_activeControllers.add(res);
return res;
}
@override
DialogWindowController createDialogWindowController({
required DialogWindowControllerDelegate delegate,
Size? preferredSize,
BoxConstraints? preferredConstraints,
BaseWindowController? parent,
String? title,
}) {
final res = DialogWindowControllerMacOS(
owner: this,
delegate: delegate,
preferredSize: preferredSize,
parent: parent,
title: title,
);
_activeControllers.add(res);
return res;
}
@internal
@override
TooltipWindowController createTooltipWindowController({
required TooltipWindowControllerDelegate delegate,
required BoxConstraints preferredConstraints,
required bool isSizedToContent,
required Rect anchorRect,
required WindowPositioner positioner,
required BaseWindowController parent,
}) {
throw UnimplementedError('Tooltip windows are not yet implemented on MacOS.');
}
@internal
@override
PopupWindowController createPopupWindowController({
required PopupWindowControllerDelegate delegate,
required BoxConstraints preferredConstraints,
required Rect anchorRect,
required WindowPositioner positioner,
required BaseWindowController parent,
}) {
throw UnimplementedError('Popup windows are not yet implemented on MacOS.');
}
final List<BaseWindowController> _activeControllers = <BaseWindowController>[];
/// Returns the window handle for the given [view], or null is the window
/// handle is not available.
///
/// The window handle is a pointer to the NSWindow instance.
static Pointer<Void> getWindowHandle(FlutterView view) {
return _MacOSPlatformInterface.getWindowHandle(
WidgetsBinding.instance.platformDispatcher.engineId!,
view.viewId,
);
}
}
/// Implementation of [RegularWindowController] for the macOS platform.
///
/// {@macro flutter.widgets.windowing.experimental}
///
/// See also:
///
/// * [RegularWindowController], the base class for regular windows.
class RegularWindowControllerMacOS extends RegularWindowController {
/// Creates a new regular window controller for macOS. When this constructor
/// completes the FlutterView is created and framework is aware of it.
RegularWindowControllerMacOS({
required WindowingOwnerMacOS owner,
required RegularWindowControllerDelegate delegate,
required Size? preferredSize,
BoxConstraints? preferredConstraints,
String? title,
}) : _owner = owner,
_delegate = delegate,
super.empty() {
if (!isWindowingEnabled) {
throw UnsupportedError(_kWindowingDisabledErrorMessage);
}
_onShouldClose = NativeCallable<Void Function()>.isolateLocal(_handleOnShouldClose);
_onWillClose = NativeCallable<Void Function()>.isolateLocal(_handleOnWillClose);
_onResize = NativeCallable<Void Function()>.isolateLocal(_handleOnResize);
final int viewId = _MacOSPlatformInterface.createRegularWindow(
preferredSize: preferredSize,
preferredConstraints: preferredConstraints,
onShouldClose: _onShouldClose.nativeFunction,
onWillClose: _onWillClose.nativeFunction,
onNotifyListeners: _onResize.nativeFunction,
);
final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere(
(FlutterView view) => view.viewId == viewId,
);
rootView = flutterView;
if (title != null) {
setTitle(title);
}
}
/// Returns window handle for the current window.
///
/// The handle is a pointer to an NSWindow instance.
Pointer<Void> getWindowHandle() {
_ensureNotDestroyed();
return WindowingOwnerMacOS.getWindowHandle(rootView);
}
bool _destroyed = false;
@override
void destroy() {
if (_destroyed) {
return;
}
final Pointer<Void> handle = getWindowHandle();
_MacOSPlatformInterface.destroyWindow(handle);
}
void _handleOnShouldClose() {
_delegate.onWindowCloseRequested(this);
}
void _handleOnWillClose() {
_destroyed = true;
_owner._activeControllers.remove(this);
_delegate.onWindowDestroyed();
_onShouldClose.close();
_onWillClose.close();
_onResize.close();
}
void _handleOnResize() {
notifyListeners();
}
@override
@internal
void setSize(Size size) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowContentSize(getWindowHandle(), size);
}
@override
@internal
void setConstraints(BoxConstraints constraints) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowConstraints(getWindowHandle(), constraints);
}
@override
void setTitle(String title) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowTitle(getWindowHandle(), title);
notifyListeners();
}
final WindowingOwnerMacOS _owner;
final RegularWindowControllerDelegate _delegate;
late final NativeCallable<Void Function()> _onShouldClose;
late final NativeCallable<Void Function()> _onWillClose;
late final NativeCallable<Void Function()> _onResize;
@override
Size get contentSize {
_ensureNotDestroyed();
return _MacOSPlatformInterface.getWindowContentSize(getWindowHandle());
}
@override
void activate() {
_ensureNotDestroyed();
_MacOSPlatformInterface.activate(getWindowHandle());
}
@override
void setMaximized(bool maximized) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setMaximized(getWindowHandle(), maximized);
}
@override
bool get isMaximized {
_ensureNotDestroyed();
return _MacOSPlatformInterface.isMaximized(getWindowHandle());
}
@override
void setMinimized(bool minimized) {
_ensureNotDestroyed();
if (minimized) {
_MacOSPlatformInterface.minimize(getWindowHandle());
} else {
_MacOSPlatformInterface.unminimize(getWindowHandle());
}
}
@override
bool get isMinimized {
_ensureNotDestroyed();
return _MacOSPlatformInterface.isMinimized(getWindowHandle());
}
@override
void setFullscreen(bool fullscreen, {Display? display}) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setFullscreen(getWindowHandle(), fullscreen);
}
@override
bool get isFullscreen {
_ensureNotDestroyed();
return _MacOSPlatformInterface.isFullscreen(getWindowHandle());
}
void _ensureNotDestroyed() {
if (_destroyed) {
throw StateError('Window has been destroyed.');
}
}
@override
bool get isActivated => _MacOSPlatformInterface.isActivated(getWindowHandle());
@override
String get title => _MacOSPlatformInterface.getTitle(getWindowHandle());
}
/// Implementation of [DialogWindowController] for the macOS platform.
///
/// {@macro flutter.widgets.windowing.experimental}
///
/// See also:
///
/// * [DialogWindowController], the base class for dialog windows.
class DialogWindowControllerMacOS extends DialogWindowController {
/// Creates a new regular window controller for macOS. When this constructor
/// completes the FlutterView is created and framework is aware of it.
DialogWindowControllerMacOS({
required WindowingOwnerMacOS owner,
required DialogWindowControllerDelegate delegate,
required Size? preferredSize,
this.parent,
BoxConstraints? preferredConstraints,
String? title,
}) : _owner = owner,
_delegate = delegate,
super.empty() {
if (!isWindowingEnabled) {
throw UnsupportedError(_kWindowingDisabledErrorMessage);
}
_onShouldClose = NativeCallable<Void Function()>.isolateLocal(_handleOnShouldClose);
_onWillClose = NativeCallable<Void Function()>.isolateLocal(_handleOnWillClose);
_onResize = NativeCallable<Void Function()>.isolateLocal(_handleOnResize);
final int viewId = _MacOSPlatformInterface.createDialogWindow(
preferredSize: preferredSize,
preferredConstraints: preferredConstraints,
onShouldClose: _onShouldClose.nativeFunction,
onWillClose: _onWillClose.nativeFunction,
onNotifyListeners: _onResize.nativeFunction,
parentViewId: parent?.rootView.viewId,
);
final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere(
(FlutterView view) => view.viewId == viewId,
);
rootView = flutterView;
if (title != null) {
setTitle(title);
}
}
/// Returns the window handle for this window.
///
/// The handle is a pointer to an `NSWindow` instance.
Pointer<Void> getWindowHandle() {
_ensureNotDestroyed();
return WindowingOwnerMacOS.getWindowHandle(rootView);
}
bool _destroyed = false;
@override
void destroy() {
if (_destroyed) {
return;
}
final Pointer<Void> handle = getWindowHandle();
_MacOSPlatformInterface.destroyWindow(handle);
}
void _handleOnShouldClose() {
_delegate.onWindowCloseRequested(this);
}
void _handleOnWillClose() {
_destroyed = true;
_owner._activeControllers.remove(this);
_delegate.onWindowDestroyed();
_onShouldClose.close();
_onWillClose.close();
_onResize.close();
}
void _handleOnResize() {
notifyListeners();
}
@override
@internal
void setSize(Size size) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowContentSize(getWindowHandle(), size);
}
@override
@internal
void setConstraints(BoxConstraints constraints) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowConstraints(getWindowHandle(), constraints);
}
@override
void setTitle(String title) {
_ensureNotDestroyed();
_MacOSPlatformInterface.setWindowTitle(getWindowHandle(), title);
notifyListeners();
}
final WindowingOwnerMacOS _owner;
final DialogWindowControllerDelegate _delegate;
late final NativeCallable<Void Function()> _onShouldClose;
late final NativeCallable<Void Function()> _onWillClose;
late final NativeCallable<Void Function()> _onResize;
@override
Size get contentSize {
_ensureNotDestroyed();
return _MacOSPlatformInterface.getWindowContentSize(getWindowHandle());
}
@override
void activate() {
_ensureNotDestroyed();
_MacOSPlatformInterface.activate(getWindowHandle());
}
@override
void setMinimized(bool minimized) {
_ensureNotDestroyed();
if (minimized) {
_MacOSPlatformInterface.minimize(getWindowHandle());
} else {
_MacOSPlatformInterface.unminimize(getWindowHandle());
}
}
@override
bool get isMinimized {
_ensureNotDestroyed();
return _MacOSPlatformInterface.isMinimized(getWindowHandle());
}
void _ensureNotDestroyed() {
if (_destroyed) {
throw StateError('Window has been destroyed.');
}
}
@override
bool get isActivated => _MacOSPlatformInterface.isActivated(getWindowHandle());
@override
String get title => _MacOSPlatformInterface.getTitle(getWindowHandle());
@override
final BaseWindowController? parent;
}
final class _WindowCreationRequest extends Struct {
@Bool()
external bool hasSize;
external _Size contentSize;
@Bool()
external bool hasConstraints;
external _Constraints constraints;
@Int64()
external int parentViewId;
external Pointer<NativeFunction<Void Function()>> onShouldClose;
external Pointer<NativeFunction<Void Function()>> onWillClose;
external Pointer<NativeFunction<Void Function()>> onNotifyListeners;
}
final class _Size extends Struct {
@Double()
external double width;
@Double()
external double height;
}
final class _Constraints extends Struct {
@Double()
external double minWidth;
@Double()
external double minHeight;
@Double()
external double maxWidth;
@Double()
external double maxHeight;
}
class _MacOSPlatformInterface {
@Native<Pointer<Void> Function(Int64, Int64)>(symbol: 'InternalFlutter_Window_GetHandle')
external static Pointer<Void> getWindowHandle(int engineId, int viewId);
@Native<Void Function(Pointer<Void>, Pointer<_Size>)>(
symbol: 'InternalFlutter_Window_SetContentSize',
)
external static void _setWindowContentSize(Pointer<Void> windowHandle, Pointer<_Size> size);
static void setWindowContentSize(Pointer<Void> windowHandle, Size size) {
final Pointer<_Size> ffiSize = _allocator<_Size>();
ffiSize.ref
..width = size.width
..height = size.height;
_setWindowContentSize(windowHandle, ffiSize);
_allocator.free(ffiSize);
}
@Native<Void Function(Pointer<Void>, Pointer<_Constraints>)>(
symbol: 'InternalFlutter_Window_SetConstraints',
)
external static void _setWindowConstraints(
Pointer<Void> windowHandle,
Pointer<_Constraints> size,
);
static void setWindowConstraints(Pointer<Void> windowHandle, BoxConstraints constraints) {
final Pointer<_Constraints> ffiConstraints = _allocator<_Constraints>();
ffiConstraints.ref
..minWidth = constraints.minWidth
..minHeight = constraints.minHeight
..maxWidth = constraints.maxWidth
..maxHeight = constraints.maxHeight;
_setWindowConstraints(windowHandle, ffiConstraints);
_allocator.free(ffiConstraints);
}
@Native<Int64 Function(Int64, Pointer<_WindowCreationRequest>)>(
symbol: 'InternalFlutter_WindowController_CreateRegularWindow',
)
external static int _createRegularWindow(int engineId, Pointer<_WindowCreationRequest> request);
/// Creates a new window and returns the viewId of the created FlutterView.
static int createRegularWindow({
required Size? preferredSize,
BoxConstraints? preferredConstraints,
required Pointer<NativeFunction<Void Function()>> onShouldClose,
required Pointer<NativeFunction<Void Function()>> onWillClose,
required Pointer<NativeFunction<Void Function()>> onNotifyListeners,
}) {
final Pointer<_WindowCreationRequest> request = _allocator<_WindowCreationRequest>()
..ref.onShouldClose = onShouldClose
..ref.onWillClose = onWillClose
..ref.onNotifyListeners = onNotifyListeners;
if (preferredSize != null) {
request.ref
..hasSize = true
..contentSize.width = preferredSize.width
..contentSize.height = preferredSize.height;
}
if (preferredConstraints != null) {
request.ref
..hasConstraints = true
..constraints.minWidth = preferredConstraints.minWidth
..constraints.minHeight = preferredConstraints.minHeight
..constraints.maxWidth = preferredConstraints.maxWidth
..constraints.maxHeight = preferredConstraints.maxHeight;
}
final int viewId = _createRegularWindow(
WidgetsBinding.instance.platformDispatcher.engineId!,
request,
);
_allocator.free(request);
return viewId;
}
@Native<Int64 Function(Int64, Pointer<_WindowCreationRequest>)>(
symbol: 'InternalFlutter_WindowController_CreateDialogWindow',
)
external static int _createDialogWindow(int engineId, Pointer<_WindowCreationRequest> request);
/// Creates a new window and returns the viewId of the created FlutterView.
static int createDialogWindow({
required Size? preferredSize,
BoxConstraints? preferredConstraints,
int? parentViewId,
required Pointer<NativeFunction<Void Function()>> onShouldClose,
required Pointer<NativeFunction<Void Function()>> onWillClose,
required Pointer<NativeFunction<Void Function()>> onNotifyListeners,
}) {
final Pointer<_WindowCreationRequest> request = _allocator<_WindowCreationRequest>()
..ref.onShouldClose = onShouldClose
..ref.onWillClose = onWillClose
..ref.onNotifyListeners = onNotifyListeners
..ref.parentViewId = parentViewId ?? 0;
if (preferredSize != null) {
request.ref
..hasSize = true
..contentSize.width = preferredSize.width
..contentSize.height = preferredSize.height;
}
if (preferredConstraints != null) {
request.ref
..hasConstraints = true
..constraints.minWidth = preferredConstraints.minWidth
..constraints.minHeight = preferredConstraints.minHeight
..constraints.maxWidth = preferredConstraints.maxWidth
..constraints.maxHeight = preferredConstraints.maxHeight;
}
try {
final int viewId = _createDialogWindow(
WidgetsBinding.instance.platformDispatcher.engineId!,
request,
);
return viewId;
} finally {
_allocator.free(request);
}
}
@Native<Void Function(Int64, Pointer<Void>)>(symbol: 'InternalFlutter_Window_Destroy')
external static void _destroyWindow(int engineId, Pointer<Void> handle);
static void destroyWindow(Pointer<Void> windowHandle) {
_destroyWindow(WidgetsBinding.instance.platformDispatcher.engineId!, windowHandle);
}
@Native<_Size Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_GetContentSize')
external static _Size _getWindowContentSize(Pointer<Void> windowHandle);
static Size getWindowContentSize(Pointer<Void> windowHandle) {
final _Size size = _getWindowContentSize(windowHandle);
return Size(size.width, size.height);
}
@Native<Void Function(Pointer<Void>, Pointer<_Utf8>)>(symbol: 'InternalFlutter_Window_SetTitle')
external static void _setWindowTitle(Pointer<Void> windowHandle, Pointer<_Utf8> title);
static void setWindowTitle(Pointer<Void> windowHandle, String title) {
final Pointer<_Utf8> titlePointer = title.toNativeUtf8();
_setWindowTitle(windowHandle, titlePointer);
_allocator.free(titlePointer);
}
@Native<Void Function(Pointer<Void>, Bool)>(symbol: 'InternalFlutter_Window_SetMaximized')
external static void setMaximized(Pointer<Void> windowHandle, bool maximized);
@Native<Bool Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_IsMaximized')
external static bool isMaximized(Pointer<Void> windowHandle);
@Native<Void Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_Minimize')
external static void minimize(Pointer<Void> windowHandle);
@Native<Void Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_Unminimize')
external static void unminimize(Pointer<Void> windowHandle);
@Native<Bool Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_IsMinimized')
external static bool isMinimized(Pointer<Void> windowHandle);
@Native<Void Function(Pointer<Void>, Bool)>(symbol: 'InternalFlutter_Window_SetFullScreen')
external static void setFullscreen(Pointer<Void> windowHandle, bool fullscreen);
@Native<Bool Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_IsFullScreen')
external static bool isFullscreen(Pointer<Void> windowHandle);
@Native<Void Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_Activate')
external static void activate(Pointer<Void> windowHandle);
@Native<Pointer<_Utf8> Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_GetTitle')
external static Pointer<_Utf8> _getTitle(Pointer<Void> windowHandle);
static String getTitle(Pointer<Void> windowHandle) {
final Pointer<_Utf8> title = _getTitle(windowHandle);
final String result = title.toDartString();
_allocator.free(title);
return result;
}
@Native<Bool Function(Pointer<Void>)>(symbol: 'InternalFlutter_Window_IsActivated')
external static bool isActivated(Pointer<Void> windowHandle);
}
// FFI utilities.
typedef _PosixCallocNative = Pointer<Void> Function(IntPtr num, IntPtr size);
@Native<_PosixCallocNative>(symbol: 'calloc')
external Pointer<Void> _posixCalloc(int num, int size);
typedef _PosixFreeNative = Void Function(Pointer<NativeType>);
@Native<Void Function(Pointer<NativeType>)>(symbol: 'free')
external void _posixFree(Pointer<NativeType> ptr);
final Pointer<NativeFunction<_PosixFreeNative>> _posixFreePointer =
Native.addressOf<NativeFunction<_PosixFreeNative>>(_posixFree);
const _CallocAllocator _allocator = _CallocAllocator._();
final class _CallocAllocator implements Allocator {
const _CallocAllocator._();
/// Allocates [byteCount] bytes of zero-initialized of memory on the native
/// heap.
@override
Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
final Pointer<T> result = _posixCalloc(byteCount, 1).cast();
if (result.address == 0) {
throw ArgumentError('Could not allocate $byteCount bytes.');
}
return result;
}
/// Releases memory allocated on the native heap.
@override
void free(Pointer<NativeType> pointer) {
_posixFree(pointer);
}
/// Returns a pointer to a native free function.
Pointer<NativeFinalizerFunction> get nativeFree => _posixFreePointer;
}
/// The contents of a native zero-terminated array of UTF-8 code units.
///
/// The Utf8 type itself has no functionality, it's only intended to be used
/// through a `Pointer<Utf8>` representing the entire array. This pointer is
/// the equivalent of a char pointer (`const char*`) in C code.
final class _Utf8 extends Opaque {}
/// Extension method for converting a`Pointer<Utf8>` to a [String].
extension _Utf8Pointer on Pointer<_Utf8> {
/// Converts this UTF-8 encoded string to a Dart string.
///
/// Decodes the UTF-8 code units of this zero-terminated byte array as
/// Unicode code points and creates a Dart string containing those code
/// points.
///
/// If [length] is provided, zero-termination is ignored and the result can
/// contain NUL characters.
///
/// If [length] is not provided, the returned string is the string up til
/// but not including the first NUL character.
String toDartString({int? length}) {
_ensureNotNullptr('toDartString');
final Pointer<Uint8> codeUnits = cast<Uint8>();
if (length != null) {
RangeError.checkNotNegative(length, 'length');
} else {
length = _length(codeUnits);
}
return utf8.decode(codeUnits.asTypedList(length));
}
static int _length(Pointer<Uint8> codeUnits) {
var length = 0;
while (codeUnits[length] != 0) {
length++;
}
return length;
}
void _ensureNotNullptr(String operation) {
if (this == nullptr) {
throw UnsupportedError("Operation '$operation' not allowed on a 'nullptr'.");
}
}
}
/// Extension method for converting a [String] to a `Pointer<Utf8>`.
extension _StringUtf8Pointer on String {
/// Creates a zero-terminated [_Utf8] code-unit array from this String.
///
/// If this [String] contains NUL characters, converting it back to a string
/// using [_Utf8Pointer.toDartString] will truncate the result if a length is
/// not passed.
///
/// Unpaired surrogate code points in this [String] will be encoded as
/// replacement characters (U+FFFD, encoded as the bytes 0xEF 0xBF 0xBD) in
/// the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
///
/// Returns an [allocator]-allocated pointer to the result.
Pointer<_Utf8> toNativeUtf8({Allocator allocator = _allocator}) {
final Uint8List units = utf8.encode(this);
final Pointer<Uint8> result = allocator<Uint8>(units.length + 1);
final Uint8List nativeString = result.asTypedList(units.length + 1);
nativeString.setAll(0, units);
nativeString[units.length] = 0;
return result.cast();
}
}