diff --git a/packages/windows_foundation/lib/src/exports.g.dart b/packages/windows_foundation/lib/src/exports.g.dart index 590bcb1e..9faac177 100644 --- a/packages/windows_foundation/lib/src/exports.g.dart +++ b/packages/windows_foundation/lib/src/exports.g.dart @@ -18,6 +18,7 @@ export 'collections/stringmap.dart'; export 'collections/valueset.dart'; export 'extensions/iunknown_helpers.dart'; export 'helpers.dart'; +export 'hstring.dart'; export 'iclosable.dart'; export 'imemorybuffer.dart'; export 'imemorybufferreference.dart'; diff --git a/packages/windows_foundation/lib/src/hstring.dart b/packages/windows_foundation/lib/src/hstring.dart new file mode 100644 index 00000000..69916133 --- /dev/null +++ b/packages/windows_foundation/lib/src/hstring.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2023, Dart | Windows. Please see the AUTHORS file for details. +// All rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// A wrapper for HSTRING types. + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +import '../internal.dart'; + +/// A string data type commonly used by Windows Runtime. +/// +/// This class wraps the Windows Runtime memory allocation functions, allowing +/// the creation of [HSTRING] types with ease, as shown below: +/// ```dart +/// final hString = HString.fromString('Hello world!'); +/// ``` +/// +/// In general, freeing the memory allocated for a [HString] when it is no +/// longer used is not a concern, as the [Finalizer] automatically releases +/// it for you when the object becomes inaccessible to the program. However, +/// you also have the option to manually release its memory by calling the +/// object's [free] method. +class HString { + /// The handle to a [HSTRING]. + final int handle; + + /// Create an empty [HString]. + // There is no need to attach a finalizer to an empty HString, as it does not + // need to be freed. + const HString.empty() : handle = 0; + + HString._(this.handle) { + _finalizer.attach(this, handle, detach: this); + } + + static final _finalizer = Finalizer(WindowsDeleteString); + + /// Create a [HString] from a given HSTRING [handle]. + factory HString.fromHandle(int handle) => + handle == 0 ? const HString.empty() : HString._(handle); + + /// Create a [HString] from a given Dart [string]. + factory HString.fromString(String string) { + if (string.isEmpty) return const HString.empty(); + return using((arena) { + final pSourceString = string.toNativeUtf16(allocator: arena); + final pString = arena(); + final hr = WindowsCreateString(pSourceString, string.length, pString); + if (FAILED(hr)) throwWindowsException(hr); + return HString._(pString.value); + }); + } + + /// Creates a copy of the existing string. + HString clone() { + if (handle == 0) return const HString.empty(); + return using((arena) { + final pNewString = arena(); + final hr = WindowsDuplicateString(handle, pNewString); + if (FAILED(hr)) throwWindowsException(hr); + return HString._(pNewString.value); + }); + } + + /// Releases the native memory allocated to the [HString]. + void free() { + _finalizer.detach(this); + WindowsDeleteString(handle); + } + + /// Whether the string is empty. + bool get isEmpty => handle == 0 ? true : WindowsIsStringEmpty(handle) == TRUE; + + /// The length of the string. + int get length => handle == 0 ? 0 : WindowsGetStringLen(handle); + + /// Concatenates two [HString]s. + HString operator +(HString other) { + return using((arena) { + final pNewString = arena(); + final hr = WindowsConcatString(handle, other.handle, pNewString); + if (FAILED(hr)) throwWindowsException(hr); + return HString._(pNewString.value); + }); + } + + /// Converts the [HString] into a regular Dart string. + @override + String toString() => handle == 0 + ? '' + : WindowsGetStringRawBuffer(handle, nullptr).toDartString(); +} diff --git a/packages/windows_foundation/test/hstring_test.dart b/packages/windows_foundation/test/hstring_test.dart new file mode 100644 index 00000000..38de9c46 --- /dev/null +++ b/packages/windows_foundation/test/hstring_test.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2023, Dart | Windows. Please see the AUTHORS file for details. +// All rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +@TestOn('windows') + +import 'package:test/test.dart'; +import 'package:win32/win32.dart'; +import 'package:windows_foundation/windows_foundation.dart'; + +void main() { + if (!isWindowsRuntimeAvailable()) { + print('Skipping tests because Windows Runtime is not available.'); + return; + } + + const testRuns = 100; + + group('HString', () { + test('allocation', () { + const testString = 'Hello world!'; + for (var i = 0; i < testRuns; i++) { + final hString = HString.fromString(testString); + expect(hString.toString(), equals(testString)); + hString.free(); + } + }); + + test('allocation of long strings', () { + final longString = 'A very long string with padding.' * 65536; + // Ten allocations is probably enough for an expensive test like this. + for (var i = 0; i < 10; i++) { + final hString = HString.fromString(longString); + expect(hString.toString(), equals(longString)); + hString.free(); + } + }); + + test('clone', () { + const testString1 = 'This message is not unique.'; + const testString2 = ''; + for (var i = 0; i < testRuns; i++) { + for (final str in [testString1, testString2]) { + final original = HString.fromString(str); + final clone = original.clone(); + expect(original.toString(), equals(clone.toString())); + expect(original.handle, equals(clone.handle)); + clone.free(); + original.free(); + } + } + }); + + test('concatenation', () { + for (var i = 0; i < testRuns; i++) { + final first = HString.fromString('Windows'); + final second = HString.fromString(' and Dart'); + final matchInHeaven = first + second; + expect(matchInHeaven.toString(), equals('Windows and Dart')); + [first, second, matchInHeaven].forEach((object) => object.free()); + } + }); + + test('empty', () { + for (var i = 0; i < testRuns; i++) { + final hString = const HString.empty(); + expect(hString.isEmpty, isTrue); + expect(hString.toString(), isEmpty); + hString.free(); + } + }); + + test('isEmpty', () { + const testString = 'dartwinrt'; + for (var i = 0; i < testRuns; i++) { + final hString1 = HString.fromString(testString); + expect(hString1.isEmpty, isFalse); + final hString2 = const HString.empty(); + expect(hString2.isEmpty, isTrue); + final hString3 = HString.fromString(''); + expect(hString3.isEmpty, isTrue); + hString1.free(); + hString2.free(); + hString3.free(); + } + }); + + test('length', () { + const testString = 'Lorem ipsum dolor sit amet, consectetur adipiscing ' + 'elit, sed do eiusmod tempor incididunt ut labore et dolore magna ' + 'aliqua.'; + for (var i = 0; i < testRuns; i++) { + final hString = HString.fromString(testString); + expect(testString.length, equals(123)); + expect(hString.length, equals(123)); + expect(hString.toString(), equals(testString)); + hString.free(); + } + }); + }); +}