Introduce C library for Pkl (#1238)

This uses native-image to generate a C library for Pkl.
This generated library from native-image is wrapped with our own library,
in `pkl.h`.

This produces a static and a dynamic library for each os/arch variant
that Pkl currently supports.

Co-authored-by: Kushal Pisavadia <kushal.p@apple.com>
Co-authored-by: Jen Basch <jbasch94@gmail.com>
Co-authored-by: Islon Scherer <i_desouzascherer@apple.com>
This commit is contained in:
Daniel Chao
2026-07-25 04:15:26 +00:00
committed by GitHub
co-authored by Kushal Pisavadia Jen Basch Islon Scherer
parent 175e2b6273
commit 67df676359
51 changed files with 4982 additions and 349 deletions
+94 -19
View File
@@ -16,49 +16,64 @@
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.ClasspathNormalizer
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFiles
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.registerIfAbsent
import org.gradle.kotlin.dsl.withNormalizer
import org.gradle.process.ExecOperations
enum class Architecture {
AMD64,
AARCH64,
}
abstract class NativeImageBuildService : BuildService<BuildServiceParameters.None>
abstract class NativeImageBuild : DefaultTask() {
@get:Input abstract val imageName: Property<String>
@get:Input abstract val outputName: Property<String>
@get:Input abstract val extraNativeImageArgs: ListProperty<String>
@get:Input abstract val arch: Property<Architecture>
@get:Input abstract val arch: Property<Target.Arch>
@get:Input abstract val mainClass: Property<String>
/**
* The main class entrypoint for the executable.
*
* This option is not necessary if [sharedLibrary] is true.
*/
@get:Optional @get:Input abstract val mainClass: Property<String>
/** Create a shared library, instead of an executable. */
@get:Input abstract val sharedLibrary: Property<Boolean>
@get:InputFiles abstract val classpath: ConfigurableFileCollection
private val outputDir = project.layout.buildDirectory.dir("executable")
@get:Input abstract val envVars: MapProperty<String, String>
@get:OutputFile val outputFile = outputDir.flatMap { it.file(imageName) }
@get:InputFile @get:Optional abstract val nativeCompilerPath: RegularFileProperty
@get:Internal abstract val outputDir: DirectoryProperty
@get:Inject protected abstract val execOperations: ExecOperations
@get:Inject protected abstract val objectFactory: ObjectFactory
private val graalVm: Provider<BuildInfo.GraalVm> = arch.map { a ->
when (a) {
Architecture.AMD64 -> buildInfo.graalVmAmd64
Architecture.AARCH64 -> buildInfo.graalVmAarch64
Target.Arch.AMD64 -> buildInfo.graalVmAmd64
Target.Arch.AARCH64 -> buildInfo.graalVmAarch64
}
}
@@ -88,6 +103,10 @@ abstract class NativeImageBuild : DefaultTask() {
// CPU resources).
usesService(buildService)
sharedLibrary.convention(false)
outputDir.convention(project.layout.buildDirectory.dir("executable"))
group = "build"
inputs
@@ -98,6 +117,51 @@ abstract class NativeImageBuild : DefaultTask() {
.files(nativeImageExecutable)
.withPropertyName("graalVmNativeImage")
.withPathSensitivity(PathSensitivity.ABSOLUTE)
if (nativeCompilerPath.isPresent) {
inputs.file(nativeCompilerPath)
}
}
@Suppress("unused")
@OutputFiles
fun getEffectiveOutputFiles(): FileCollection {
return objectFactory
.fileCollection()
.from(
sharedLibrary.map { isLibrary ->
val dir = outputDir.get()
val libraryName = outputName.get()
// if building a library, native-image outputs the shared library plus four headers
// (and, on Windows, an import library); otherwise, it outputs just the one file
if (isLibrary) {
val sharedLibraryExtension = buildInfo.os.sharedLibraryExtension
val staticLibraryExtension = buildInfo.os.staticLibraryExtension
dir.files(
buildList {
add("$libraryName.$sharedLibraryExtension")
if (buildInfo.os.isWindows) {
// the DLL's import library, which consumers must link against instead of
// the .dll itself
add("$libraryName.$staticLibraryExtension")
// a genuine, self-contained static archive with no runtime DLL dependency,
// produced by build_windows.bat
add("${libraryName}_s.$staticLibraryExtension")
} else {
// a genuine, self-contained static archive with no runtime dependency,
// produced by build_unix.sh
add("$libraryName.$staticLibraryExtension")
}
add("$libraryName.h")
add("${libraryName}_dynamic.h")
add("graal_isolate.h")
add("graal_isolate_dynamic.h")
}
)
} else {
dir.file(libraryName)
}
}
)
}
@TaskAction
@@ -111,6 +175,7 @@ abstract class NativeImageBuild : DefaultTask() {
workingDir(outputDir)
args = buildList {
add("--color=always")
// must be emitted before any experimental options are used
add("-H:+UnlockExperimentalVMOptions")
// currently gives a deprecation warning, but we've been told
@@ -126,9 +191,14 @@ abstract class NativeImageBuild : DefaultTask() {
add("-H:IncludeResourceBundles=org.pkl.core.errorMessages")
add("-H:IncludeResourceBundles=org.pkl.parser.errorMessages")
add("-H:IncludeResources=org/pkl/commons/cli/PklCARoots.pem")
add("-H:Class=${mainClass.get()}")
if (mainClass.isPresent) {
add("-H:Class=${mainClass.get()}")
}
if (sharedLibrary.get()) {
add("--shared")
}
add("-o")
add(imageName.get())
add(outputName.get())
// the actual limit (currently) used by native-image is this number + 1400 (idea is to
// compensate for Truffle's own nodes)
add("-H:MaxRuntimeCompileMethods=1800")
@@ -138,10 +208,11 @@ abstract class NativeImageBuild : DefaultTask() {
// disable automatic support for JVM CLI options (puts our main class in full control of
// argument parsing)
add("-H:-ParseRuntimeOptions")
// quick build mode: 40% faster compilation, 20% smaller (but presumably also slower)
// executable
if (!buildInfo.isReleaseBuild) {
add("-Ob")
// disable all optimizations
add("-O0")
// generate debugging information
add("-g")
}
if (buildInfo.isNativeArch) {
add("-march=native")
@@ -157,8 +228,12 @@ abstract class NativeImageBuild : DefaultTask() {
// make sure dev machine stays responsive (15% slowdown on my laptop)
val processors =
Runtime.getRuntime().availableProcessors() /
if (buildInfo.os.isMacOsX && !buildInfo.isCiBuild) 4 else 1
if (buildInfo.os.isMacOS && !buildInfo.isCiBuild) 4 else 1
add("-J-XX:ActiveProcessorCount=${processors}")
addAll(envVars.get().entries.map { "-E${it.key}=${it.value}" })
if (nativeCompilerPath.isPresent) {
add("--native-compiler-path=${nativeCompilerPath.get().asFile}")
}
// Pass through all `HOMEBREW_` prefixed environment variables to allow build with shimmed
// tools.
addAll(environment.keys.filter { it.startsWith("HOMEBREW_") }.map { "-E$it" })