Files
pkl/libpkl/src/main/c/pkl.c
T
Daniel Chao 417c08ae11 Fix various issues in libpkl (#1805)
* Fix possible SIGSEGV from host process.
   - Require that the same `pkl_exec_t` be used in the same OS thread; returning meaningful error if this fails
* Remove `System.exitProcess(1)` logic in NativeTransport; this would kill the host process too and isn't an appropriate action for a received ProtocolException
* Allow multiple calls to pkl_init without pkl_close; this limitation doesn't really make any sense
* Prefer calling methods defined in graal_isolate.h, instead of creating the same method via CEntryPoint
* Add a CMakeLists.txt so that CLion can follow the code and provide proper diagnostics
* Improve doc comments; describe params as either in or out
2026-07-31 16:23:37 +00:00

253 lines
5.9 KiB
C

/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// pkl.c
#include <stdlib.h>
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <pthread.h>
#endif
#include <graal_isolate.h>
#include <libpkl_internal.h>
#include <pkl.h>
#ifndef PKL_VERSION
#define PKL_VERSION "0.0.0"
#endif
// ReSharper disable once CppClassNeverUsed
struct __pkl_exec_t {
graal_isolate_t *isolate;
graal_isolatethread_t *isolateThread;
/** The caller-supplied handler/userData from pkl_init, invoked only from `queue_thread`. */
pkl_message_response_handler handler;
void *userData;
#ifdef _WIN32
HANDLE queue_thread;
#else
pthread_t queue_thread;
#endif
};
/**
* Polls for messages coming from Pkl and sends them back to the handler.
*
* Ensures that calls into Pkl are wrapped with graal_attach_thread/graal_detach_thread, and
* calls back to the handler are _not_ inside the GraalVM thread context.
*/
#ifdef _WIN32
static DWORD WINAPI pkl_dispatch_worker(LPVOID arg) {
#else
static void* pkl_dispatch_worker(void *arg) {
#endif
const pkl_exec_t *pexec = (pkl_exec_t*) arg;
for (;;) {
graal_isolatethread_t *thread;
if (graal_attach_thread(pexec->isolate, &thread) != 0) {
fprintf(stderr,
"fatal: failed to attach response dispatch thread to isolate.\n");
abort();
}
char *message = NULL;
const int length = pkl_internal_poll_response(thread, &message);
if (graal_detach_thread(thread) != 0) {
fprintf(stderr,
"fatal: failed to detach response dispatch thread from isolate.\n");
abort();
}
if (length < 0) {
// pkl_internal_server_stop has been called and the queue is drained
break;
}
pexec->handler(length, message, pexec->userData);
// message was allocated on the Java side via UnmanagedMemory.malloc, which maps onto
// the platform's malloc/free; ownership passes to us here.
free(message);
}
#ifdef _WIN32
return 0;
#else
return NULL;
#endif
}
static int pkl_start_dispatch_worker(pkl_exec_t *pexec, pkl_error_t *error) {
#ifdef _WIN32
pexec->queue_thread = CreateThread(NULL, 0, pkl_dispatch_worker, pexec, 0, NULL);
if (pexec->queue_thread == NULL) {
#else
if (pthread_create(&pexec->queue_thread, NULL, pkl_dispatch_worker, pexec)
!= 0) {
#endif
if (error != NULL) {
error->message = "Failed to create response dispatch thread";
}
return -1;
}
return 0;
}
static void pkl_stop_dispatch_worker(pkl_exec_t *pexec) {
#ifdef _WIN32
WaitForSingleObject(pexec->queue_thread, INFINITE);
CloseHandle(pexec->queue_thread);
#else
pthread_join(pexec->queue_thread, NULL);
#endif
}
int pkl_init(pkl_message_response_handler handler, void *userData,
pkl_exec_t **exec, pkl_error_t *error) {
if (handler == NULL) {
if (error != NULL) {
error->message = "handler is null";
}
return -1;
}
if (exec == NULL) {
if (error != NULL) {
error->message = "exec is null";
}
return -1;
}
pkl_exec_t *pexec = calloc(1, sizeof(pkl_exec_t));
if (pexec == NULL) {
fprintf(stderr, "failed to allocate pkl_exec_t\n");
abort();
}
pexec->handler = handler;
pexec->userData = userData;
graal_isolate_t *isolate;
graal_isolatethread_t *isolateThread;
if (graal_create_isolate(NULL, &isolate, &isolateThread) != 0) {
if (error != NULL) {
error->message = "Failed to create graal isolate thread";
}
free(pexec);
*exec = NULL;
return -1;
}
pexec->isolate = isolate;
pexec->isolateThread = isolateThread;
if (pkl_start_dispatch_worker(pexec, error) != 0) {
graal_tear_down_isolate(isolateThread);
free(pexec);
*exec = NULL;
return -1;
}
pkl_internal_server_start(isolateThread);
*exec = pexec;
if (error != NULL) {
error->message = NULL;
}
return 0;
}
int pkl_send_message(const pkl_exec_t *pexec, const unsigned int length,
char *message, pkl_error_t *error) {
if (pexec == NULL) {
if (error != NULL) {
error->message = "pexec is null";
}
return -1;
}
if (message == NULL) {
if (error != NULL) {
error->message = "message is null";
}
return -1;
}
graal_isolatethread_t *thread = graal_get_current_thread(pexec->isolate);
if (thread != pexec->isolateThread) {
if (error != NULL) {
error->message =
"called into pkl_send_message from different thread";
}
return PKL_ERR_THREAD;
}
char *errormessage = NULL;
const int resp = pkl_internal_send_message(thread, (int) length, message,
&errormessage);
if (resp != 0) {
if (error != NULL) {
error->message = errormessage;
}
return resp;
}
if (error != NULL) {
error->message = NULL;
}
return 0;
}
int pkl_close(pkl_exec_t *pexec, pkl_error_t *error) {
if (pexec == NULL) {
if (error != NULL) {
error->message = "pexec is null";
}
return -1;
}
graal_isolatethread_t *thread = graal_get_current_thread(pexec->isolate);
if (thread != pexec->isolateThread) {
if (error != NULL) {
error->message = "called into pkl_close from different thread";
}
return PKL_ERR_THREAD;
}
// pkl_internal_server_stop unblocks queue_thread's pending/next poll call, so the join
// below is guaranteed to return; only then is it safe to tear down the isolate.
pkl_internal_server_stop(thread);
pkl_stop_dispatch_worker(pexec);
if (graal_tear_down_isolate(thread) != 0) {
fprintf(stderr, "fatal: failed to tear down graal isolate.\n");
abort();
}
free(pexec);
if (error != NULL) {
error->message = NULL;
}
return 0;
}
const char* pkl_version() {
return PKL_VERSION;
}