-
Notifications
You must be signed in to change notification settings - Fork 110
Wasm/WASI target implementation #366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
65075c7
cea3246
1233b92
571106d
efc8227
61f1e60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import java.io.File | ||
|
||
/* | ||
* Copyright 2019-2024 JetBrains s.r.o. and contributors. | ||
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. | ||
*/ | ||
|
||
private val pkg = "package kotlinx.datetime.timezones.tzData" | ||
|
||
private fun generateByteArrayProperty(tzData: TzData, header: String, propertyName: String): String = buildString { | ||
append(header) | ||
appendLine() | ||
appendLine() | ||
appendLine("/* ${tzData.fullTzNames.joinToString(", ")} */") | ||
append("internal val $propertyName get() = byteArrayOf(") | ||
for (chunk in tzData.data.toList().chunked(16)) { | ||
appendLine() | ||
append(" ") | ||
val chunkText = chunk.joinToString { | ||
it.toString().padStart(4, ' ') | ||
} + "," | ||
append(chunkText) | ||
} | ||
appendLine() | ||
append(")") | ||
} | ||
|
||
private class TzData(val data: ByteArray, val fullTzNames: MutableList<String>) | ||
private fun loadTzBinaries( | ||
zoneInfo: File, | ||
currentName: String, | ||
result: MutableList<TzData> | ||
) { | ||
val zoneName = if (currentName.isEmpty()) zoneInfo.name else "$currentName/${zoneInfo.name}" | ||
if (zoneInfo.isDirectory) { | ||
zoneInfo.listFiles()?.forEach { | ||
loadTzBinaries(it, zoneName, result) | ||
} | ||
} else { | ||
val bytes = zoneInfo.readBytes() | ||
val foundTzData = result.firstOrNull { it.data.contentEquals(bytes) } | ||
val tzData: TzData | ||
if (foundTzData != null) { | ||
tzData = foundTzData | ||
} else { | ||
tzData = TzData(bytes, mutableListOf()) | ||
result.add(tzData) | ||
} | ||
|
||
tzData.fullTzNames.add(zoneName) | ||
} | ||
} | ||
|
||
fun generateZoneInfosResources(zoneInfoDir: File, outputDir: File, version: String) { | ||
val header = buildString { | ||
appendLine() | ||
dkhalanskyjb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
append("/* AUTOGENERATED FROM ZONE INFO DATABASE v.$version */") | ||
appendLine() | ||
appendLine() | ||
append(pkg) | ||
} | ||
|
||
val loadedZones = mutableListOf<TzData>() | ||
zoneInfoDir.listFiles()?.forEach { file -> | ||
loadTzBinaries(file, "", loadedZones) | ||
} | ||
|
||
val zoneDataByNameBody = StringBuilder() | ||
val getTimeZonesBody = StringBuilder() | ||
loadedZones.forEachIndexed { id, tzData -> | ||
val tzDataName = "tzData$id" | ||
val data = generateByteArrayProperty(tzData, header, tzDataName) | ||
File(outputDir, "$tzDataName.kt").writeText(data) | ||
tzData.fullTzNames.forEach { name -> | ||
zoneDataByNameBody.appendLine(" \"$name\" -> $tzDataName") | ||
getTimeZonesBody.appendLine(" \"$name\",") | ||
} | ||
} | ||
|
||
val content = buildString { | ||
append(header) | ||
appendLine() | ||
appendLine() | ||
appendLine("internal fun zoneDataByName(name: String): ByteArray = when(name) {") | ||
dkhalanskyjb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
append(zoneDataByNameBody) | ||
appendLine() | ||
append(" else -> throw kotlinx.datetime.IllegalTimeZoneException(\"Invalid timezone name\")") | ||
appendLine() | ||
append("}") | ||
appendLine() | ||
appendLine() | ||
append("internal val timeZones: Set<String> by lazy { setOf(") | ||
appendLine() | ||
append(getTimeZonesBody) | ||
appendLine() | ||
append(")") | ||
append("}") | ||
} | ||
|
||
File(outputDir, "tzData.kt").writeText(content) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* Copyright 2019-2024 JetBrains s.r.o. and contributors. | ||
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. | ||
*/ | ||
|
||
package kotlinx.datetime.internal | ||
|
||
import kotlinx.datetime.Instant | ||
|
||
internal expect val systemTzdb: TimeZoneDatabase | ||
|
||
internal expect fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> | ||
|
||
internal expect fun currentTime(): Instant |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright 2019-2024 JetBrains s.r.o. and contributors. | ||
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. | ||
*/ | ||
|
||
package kotlinx.datetime.internal | ||
|
||
import kotlinx.datetime.Instant | ||
import kotlin.wasm.WasmImport | ||
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi | ||
import kotlin.wasm.unsafe.withScopedMemoryAllocator | ||
|
||
/** | ||
* Return the time value of a clock. Note: This is similar to `clock_gettime` in POSIX. | ||
*/ | ||
@WasmImport("wasi_snapshot_preview1", "clock_time_get") | ||
private external fun wasiRawClockTimeGet(clockId: Int, precision: Long, resultPtr: Int): Int | ||
|
||
private const val CLOCKID_REALTIME = 0 | ||
|
||
@OptIn(UnsafeWasmMemoryApi::class) | ||
private fun clockTimeGet(): Long = withScopedMemoryAllocator { allocator -> | ||
val rp0 = allocator.allocate(8) | ||
val ret = wasiRawClockTimeGet( | ||
clockId = CLOCKID_REALTIME, | ||
precision = 1, | ||
resultPtr = rp0.address.toInt() | ||
) | ||
if (ret == 0) { | ||
rp0.loadLong() | ||
} else { | ||
error("WASI call failed with $ret") | ||
} | ||
} | ||
|
||
internal actual fun currentTime(): Instant = clockTimeGet().let { time -> | ||
// Instant.MAX and Instant.MIN are never going to be exceeded using just the Long number of nanoseconds | ||
Instant(time.floorDiv(NANOS_PER_ONE.toLong()), time.mod(NANOS_PER_ONE.toLong()).toInt()) | ||
} | ||
|
||
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = | ||
"UTC" to null | ||
|
||
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright 2019-2023 JetBrains s.r.o. and contributors. | ||
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. | ||
*/ | ||
|
||
package kotlinx.datetime.internal | ||
|
||
import kotlinx.datetime.IllegalTimeZoneException | ||
|
||
@RequiresOptIn | ||
internal annotation class InternalDateTimeApi | ||
|
||
/* | ||
This is internal API which is not intended to use on user-side. | ||
*/ | ||
dkhalanskyjb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@InternalDateTimeApi | ||
public interface TimeZonesProvider { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you mark this as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, I would prefer not suppressing anything in libraries. Only with special request from libraries team. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We prioritize user experience over how clean our internal implementation is, so yes, if it's possible to avoid extra public entry points in a robust manner, we try to do so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note that accessing internals outside of the module is an error, not a warning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, in that case, why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suppressing such errors is not advisable by the compiler team and can stop working in K2 |
||
public fun zoneDataByName(name: String): ByteArray | ||
public fun getTimeZones(): Set<String> | ||
} | ||
|
||
/* | ||
This is internal API which is not intended to use on user-side. | ||
*/ | ||
@InternalDateTimeApi | ||
public fun initializeTimeZonesProvider(provider: TimeZonesProvider) { | ||
check(timeZonesProvider != provider) { "TimeZone database redeclaration" } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to decide what to do if more that one timezone library were added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this issue can't occur currently, when there's only one artifact with the time zones, right? If we decide to publish partial timezone data, we could do it in a way that resolves conflicts on the build system level. For example, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no idea how to to resolve such conflicts for different flavors of timezones libraries without a special gradle plugin. We, with gradle plugin team, do not see the way for now, how to get one dependency with gradle resolution, the only way I see now is to allow redeclaration in runtime with a special "priority" level. But how to manage this level's needs a consideration. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's a scheme:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, for now we are not decided yet what could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In any case, we'll be able to work around this somehow. This is a technical thing invisible to the users, so we can safely do anything we want here. |
||
timeZonesProvider = provider | ||
} | ||
|
||
@InternalDateTimeApi | ||
private var timeZonesProvider: TimeZonesProvider? = null | ||
|
||
@OptIn(InternalDateTimeApi::class) | ||
internal class TzdbOnData: TimeZoneDatabase { | ||
override fun rulesForId(id: String): TimeZoneRules { | ||
val data = timeZonesProvider?.zoneDataByName(id) | ||
?: throw IllegalTimeZoneException("TimeZones are not supported") | ||
return readTzFile(data).toTimeZoneRules() | ||
} | ||
|
||
override fun availableTimeZoneIds(): Set<String> = | ||
timeZonesProvider?.getTimeZones() ?: setOf("UTC") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
org.gradle.jvmargs=-Xmx1G -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 | ||
org.gradle.jvmargs=-Xmx2G -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 | ||
org.gradle.java.installations.fromEnv=JDK_8 | ||
|
||
group=org.jetbrains.kotlinx | ||
version=0.6.1 | ||
versionSuffix=SNAPSHOT | ||
|
||
tzdbVersion=2024a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note (we'll have to discuss this internally): SemVer doesn't allow letters in the initial components, so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the end, this isn't a SemVer anyway, so dashes aren't needed, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may be out of this PR's scope, but it's possible to automatically detect that a new version of tzdb was published: https://github.com/ThreeTen/threetenbp/blob/main/.github/workflows/tzdbupdate.yml If you feel like implementing this, it would be nice; if not, we'll just do it later ourselves. |
||
|
||
defaultKotlinVersion=1.9.21 | ||
dokkaVersion=1.9.20 | ||
serializationVersion=1.6.2 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.