@gjsify/node-gi 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Android only: bring GDK's Android backend up without GTK's RuntimeApplication.
3
+ //
4
+ // GDK's Android backend is initialized by gdk_android_initialize(env, classloader,
5
+ // context) — the only exported entry point into it; everything else it needs
6
+ // (gdk_android_set_latest_activity in particular) is hidden. GTK ships one caller:
7
+ // JNI_OnLoad in libgtk-4.so, which runs when something calls
8
+ // System.loadLibrary("gtk-4") and which passes NULL for the context, plus
9
+ // RuntimeApplication.startRuntime(), which passes the android.app.Application — and
10
+ // also spawns GTK's own thread and dlopens an application library with a main().
11
+ //
12
+ // A JS host cannot use either. It is not the Application (NativeScript is), so
13
+ // startRuntime is out; and the NULL context from JNI_OnLoad is not enough: gtk_init
14
+ // calls Context.getSystemService() on it and the process dies with
15
+ // "JNI DETECTED ERROR IN APPLICATION: can't call ... getSystemService ... on null
16
+ // object". GIRepository's own dlopen of libgtk-4.so does not run JNI_OnLoad at all.
17
+ //
18
+ // So node-gi calls gdk_android_initialize itself, with the context the host names:
19
+ // the same (Application, app classloader) pair startRuntime uses, and nothing else of
20
+ // it. GTK's comment on that call — "This is *really* questionable, as thiz isn't
21
+ // actually an activity. I've updated the code to handle this case" — is the upstream
22
+ // licence for an Application here: it carries gtk_init over until a real
23
+ // ToplevelActivity reports itself through GdkContext._set_latest_activity().
24
+ //
25
+ // Opt-in, never automatic: dlopening libgtk-4.so pulls in the whole GTK stack, and a
26
+ // host that only wants GLib must not pay for it. See __NODE_GI_ANDROID_GDK in
27
+ // host.nativescript.js.
28
+
29
+ #include "common.h"
30
+
31
+ #ifdef __ANDROID__
32
+
33
+ #include <dlfcn.h>
34
+ #include <jni.h>
35
+
36
+ #include <mutex>
37
+ #include <string>
38
+
39
+ // Both set by JNI_OnLoad at the bottom of this file; file-static so both ends see them.
40
+ // The bootstrap class name is the only thing that travels the other way: JNI_OnLoad needs
41
+ // it to find the app's classloader, and only the host knows what the app called it.
42
+ static JavaVM* g_vm = nullptr;
43
+ static jobject g_app_loader = nullptr;
44
+ static std::string g_bootstrap_class;
45
+
46
+ namespace nodegi {
47
+
48
+ namespace {
49
+
50
+ // Describe-and-clear: the exception text is the whole diagnosis, and leaving it pending
51
+ // turns the next JNI call into an abort.
52
+ bool Failed(JNIEnv* env) {
53
+ if (env->ExceptionCheck() == JNI_FALSE) return false;
54
+ env->ExceptionDescribe();
55
+ env->ExceptionClear();
56
+ return true;
57
+ }
58
+
59
+ // App classes are invisible to FindClass from here: with no Java frame below the Node-API
60
+ // call, ART resolves against the system classloader, which does not know com.tns.* — the
61
+ // same reason a one-argument Class.forName() fails on this thread. Hence the loader
62
+ // JNI_OnLoad cached, with the thread's context loader and plain FindClass behind it for a
63
+ // host whose classes the system loader does have.
64
+ jclass LoadAppClass(JNIEnv* env, const std::string& jni_name) {
65
+ std::string dotted = jni_name;
66
+ for (char& c : dotted)
67
+ if (c == '/') c = '.';
68
+
69
+ jobject loader = g_app_loader;
70
+ if (loader == nullptr) {
71
+ jclass thread_class = env->FindClass("java/lang/Thread");
72
+ jmethodID current =
73
+ env->GetStaticMethodID(thread_class, "currentThread", "()Ljava/lang/Thread;");
74
+ jobject thread = env->CallStaticObjectMethod(thread_class, current);
75
+ if (Failed(env) || thread == nullptr) return nullptr;
76
+ jmethodID get_loader = env->GetMethodID(thread_class, "getContextClassLoader",
77
+ "()Ljava/lang/ClassLoader;");
78
+ loader = env->CallObjectMethod(thread, get_loader);
79
+ if (Failed(env) || loader == nullptr)
80
+ return static_cast<jclass>(env->FindClass(jni_name.c_str()));
81
+ }
82
+
83
+ jclass loader_class = env->FindClass("java/lang/ClassLoader");
84
+ jmethodID load_class = env->GetMethodID(loader_class, "loadClass",
85
+ "(Ljava/lang/String;)Ljava/lang/Class;");
86
+ jstring name = env->NewStringUTF(dotted.c_str());
87
+ auto found = static_cast<jclass>(env->CallObjectMethod(loader, load_class, name));
88
+ if (Failed(env) || found == nullptr)
89
+ return static_cast<jclass>(env->FindClass(jni_name.c_str()));
90
+ return found;
91
+ }
92
+
93
+ // androidHasJavaVm() -> boolean
94
+ //
95
+ // Whether JNI_OnLoad has run, i.e. whether the addon has a JavaVM and the app's
96
+ // classloader. Both arrive together and neither can be obtained any other way, so this
97
+ // one answer covers both — the host asks before and after the bootstrap load.
98
+ Napi::Value AndroidHasJavaVm(const Napi::CallbackInfo& info) {
99
+ return Napi::Boolean::New(info.Env(), g_vm != nullptr);
100
+ }
101
+
102
+ // androidRegisterJniBootstrap(jniClassName) -> undefined
103
+ //
104
+ // Names the class the host is about to initialize to trigger JNI_OnLoad, in JNI form
105
+ // ('dev/example/JniBootstrap'). JNI_OnLoad looks it up to reach the app's classloader:
106
+ // during a load that ART performs, FindClass resolves against the classloader of the
107
+ // class that called System.loadLibrary, which is the one moment app classes are visible
108
+ // to this library. Must therefore be called BEFORE that load.
109
+ Napi::Value AndroidRegisterJniBootstrap(const Napi::CallbackInfo& info) {
110
+ Napi::Env env = info.Env();
111
+ if (info.Length() < 1 || !info[0].IsString()) {
112
+ Napi::TypeError::New(env, "androidRegisterJniBootstrap(jniClassName): a string is required")
113
+ .ThrowAsJavaScriptException();
114
+ return env.Undefined();
115
+ }
116
+ g_bootstrap_class = info[0].As<Napi::String>().Utf8Value();
117
+ return env.Undefined();
118
+ }
119
+
120
+ bool g_initialized = false;
121
+ // Both JS threads of a NativeScript app share this library — the UI thread and a Worker,
122
+ // and in the stage-5 spike both import node-gi — so the one-time guard needs a lock of its
123
+ // own: gdk_android_initialize has none, and a second run leaks another set of global refs
124
+ // and re-registers every native.
125
+ std::mutex g_init_mutex;
126
+
127
+ // androidInitGdk(contextClass, contextMethod, contextSignature) -> boolean
128
+ //
129
+ // contextClass/contextMethod name a static method returning the android.content.Context
130
+ // to hand GDK, in JNI form — e.g. ('com/tns/NativeScriptApplication', 'getInstance',
131
+ // '()Landroid/app/Application;'). The host names it because only the host knows how to
132
+ // reach its Application without @hide APIs (android.app.ActivityThread is one).
133
+ // Idempotent, per process rather than per thread — see g_init_mutex.
134
+ Napi::Value AndroidInitGdk(const Napi::CallbackInfo& info) {
135
+ Napi::Env napi_env = info.Env();
136
+ std::lock_guard<std::mutex> guard(g_init_mutex);
137
+ if (g_initialized) return Napi::Boolean::New(napi_env, true);
138
+
139
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
140
+ Napi::TypeError::New(napi_env,
141
+ "androidInitGdk(contextClass, contextMethod, contextSignature): "
142
+ "three strings in JNI form are required")
143
+ .ThrowAsJavaScriptException();
144
+ return napi_env.Undefined();
145
+ }
146
+ std::string class_name = info[0].As<Napi::String>().Utf8Value();
147
+ std::string method_name = info[1].As<Napi::String>().Utf8Value();
148
+ std::string signature = info[2].As<Napi::String>().Utf8Value();
149
+
150
+ if (g_vm == nullptr) {
151
+ Napi::Error::New(napi_env,
152
+ "androidInitGdk: no JavaVM — this addon's JNI_OnLoad never ran. "
153
+ "Initialize an app class that calls System.loadLibrary(\"node_gi\") "
154
+ "first; a dlopen of the library does not run it.")
155
+ .ThrowAsJavaScriptException();
156
+ return napi_env.Undefined();
157
+ }
158
+
159
+ // A thread this call attaches has to be detached again: ART aborts a thread that exits
160
+ // while still attached ("Native thread exiting without having called
161
+ // DetachCurrentThread"). Safe because gdk_android_initialize keeps the JavaVM and global
162
+ // refs, never a JNIEnv, and GDK reattaches per call through its own thread-env guard.
163
+ JNIEnv* env = nullptr;
164
+ bool attached = false;
165
+ if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
166
+ if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK)
167
+ attached = true;
168
+ else
169
+ env = nullptr;
170
+ }
171
+ if (env == nullptr) {
172
+ Napi::Error::New(napi_env, "androidInitGdk: could not attach this thread to the JavaVM")
173
+ .ThrowAsJavaScriptException();
174
+ return napi_env.Undefined();
175
+ }
176
+
177
+ // gdk_android_initialize keeps global refs to both arguments, so a local frame is
178
+ // enough and nothing here outlives it.
179
+ env->PushLocalFrame(16);
180
+ std::string error;
181
+ do {
182
+ jclass provider = LoadAppClass(env, class_name);
183
+ if (Failed(env) || provider == nullptr) {
184
+ error = "androidInitGdk: no class " + class_name + " on this thread's classloader";
185
+ break;
186
+ }
187
+ jmethodID getter = env->GetStaticMethodID(provider, method_name.c_str(), signature.c_str());
188
+ if (Failed(env) || getter == nullptr) {
189
+ error = "androidInitGdk: " + class_name + " has no static " + method_name + signature;
190
+ break;
191
+ }
192
+ jobject context = env->CallStaticObjectMethod(provider, getter);
193
+ if (Failed(env) || context == nullptr) {
194
+ error = "androidInitGdk: " + class_name + "." + method_name + "() returned no Context";
195
+ break;
196
+ }
197
+
198
+ // The classloader GDK resolves org.gtk.android.* with has to be the one that holds
199
+ // them: the loader of the app's own classes, which is the context's.
200
+ jclass class_class = env->FindClass("java/lang/Class");
201
+ jmethodID get_loader = env->GetMethodID(class_class, "getClassLoader",
202
+ "()Ljava/lang/ClassLoader;");
203
+ jobject loader = env->CallObjectMethod(env->GetObjectClass(context), get_loader);
204
+ if (Failed(env) || loader == nullptr) {
205
+ error = "androidInitGdk: the Context's class has no classloader";
206
+ break;
207
+ }
208
+
209
+ // RTLD_GLOBAL: the backend resolves GDK symbols out of this handle later, and
210
+ // GIRepository may have loaded libgtk-4.so locally already.
211
+ void* gtk = dlopen("libgtk-4.so", RTLD_NOW | RTLD_GLOBAL);
212
+ if (gtk == nullptr) {
213
+ error = std::string("androidInitGdk: dlopen(libgtk-4.so) failed: ") + dlerror();
214
+ break;
215
+ }
216
+ using InitFn = int (*)(JNIEnv*, jobject, jobject);
217
+ auto initialize = reinterpret_cast<InitFn>(dlsym(gtk, "gdk_android_initialize"));
218
+ if (initialize == nullptr) {
219
+ error = "androidInitGdk: libgtk-4.so exports no gdk_android_initialize "
220
+ "(GTK built without the Android backend?)";
221
+ break;
222
+ }
223
+ if (initialize(env, loader, context) == 0) {
224
+ error = "androidInitGdk: gdk_android_initialize() refused — see the GTK log";
225
+ break;
226
+ }
227
+ g_initialized = true;
228
+ } while (false);
229
+ env->PopLocalFrame(nullptr);
230
+ if (attached) g_vm->DetachCurrentThread();
231
+
232
+ if (!g_initialized) {
233
+ Napi::Error::New(napi_env, error).ThrowAsJavaScriptException();
234
+ return napi_env.Undefined();
235
+ }
236
+ return Napi::Boolean::New(napi_env, true);
237
+ }
238
+
239
+ } // namespace
240
+
241
+ void InitAndroidGdk(Napi::Env env, Napi::Object exports) {
242
+ exports.Set("androidHasJavaVm", Napi::Function::New(env, AndroidHasJavaVm));
243
+ exports.Set("androidRegisterJniBootstrap",
244
+ Napi::Function::New(env, AndroidRegisterJniBootstrap));
245
+ exports.Set("androidInitGdk", Napi::Function::New(env, AndroidInitGdk));
246
+ }
247
+
248
+ } // namespace nodegi
249
+
250
+ // Runs when ART loads this library on behalf of a class — not on the dlopen that the JS
251
+ // runtime does. Two things are available here and nowhere else: the JavaVM, and a
252
+ // FindClass that resolves against the app's classloader rather than the system one.
253
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
254
+ g_vm = vm;
255
+ if (g_bootstrap_class.empty()) return JNI_VERSION_1_6;
256
+
257
+ JNIEnv* env = nullptr;
258
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK || env == nullptr)
259
+ return JNI_VERSION_1_6;
260
+
261
+ env->PushLocalFrame(4);
262
+ jclass bootstrap = env->FindClass(g_bootstrap_class.c_str());
263
+ if (!nodegi::Failed(env) && bootstrap != nullptr) {
264
+ jmethodID get_loader = env->GetMethodID(env->GetObjectClass(bootstrap), "getClassLoader",
265
+ "()Ljava/lang/ClassLoader;");
266
+ jobject loader = env->CallObjectMethod(bootstrap, get_loader);
267
+ if (!nodegi::Failed(env) && loader != nullptr) g_app_loader = env->NewGlobalRef(loader);
268
+ }
269
+ env->PopLocalFrame(nullptr);
270
+ return JNI_VERSION_1_6;
271
+ }
272
+
273
+ #else
274
+
275
+ namespace nodegi {
276
+
277
+ // Off Android there is no JavaVM and no Android GDK backend to initialize; the exports
278
+ // stay so a host can ask without branching on the platform first.
279
+ static Napi::Value AndroidFalse(const Napi::CallbackInfo& info) {
280
+ return Napi::Boolean::New(info.Env(), false);
281
+ }
282
+
283
+ void InitAndroidGdk(Napi::Env env, Napi::Object exports) {
284
+ exports.Set("androidHasJavaVm", Napi::Function::New(env, AndroidFalse));
285
+ exports.Set("androidRegisterJniBootstrap", Napi::Function::New(env, AndroidFalse));
286
+ exports.Set("androidInitGdk", Napi::Function::New(env, AndroidFalse));
287
+ }
288
+
289
+ } // namespace nodegi
290
+
291
+ #endif
@@ -0,0 +1,187 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Android only: route GLib's messages into logcat.
3
+ //
4
+ // On Android an app's stdout/stderr go to /dev/null unless something redirects them,
5
+ // so GLib's default handlers — which write to stderr — lose every g_warning,
6
+ // g_critical and g_message a GI call produces. The failure mode is the expensive one:
7
+ // GTK reports precisely why it refused to start, and the report never leaves the
8
+ // process. GTK's own Android runtime installs these four handlers in startRuntime()
9
+ // (refs/gtk/gdk/android/gdkandroidruntime.c), but startRuntime also loads an
10
+ // application library and spawns GTK's own thread, so a node-gi host must not call
11
+ // it — this is the piece of it that a host still needs.
12
+ //
13
+ // Installed from Init, not on request: a host that has not asked for logging still
14
+ // wants to be told why GI failed, and there is no other sink on this platform.
15
+ // g_log_set_default_handler / g_log_set_writer_func are process-global and
16
+ // last-call-wins, so a later logSetWriterFunc (private.cc) still overrides this.
17
+ //
18
+ // GLib is not the only writer: a foreign C library that does fprintf(stderr, ...) and
19
+ // abort() (libepoxy's "eglQueryDisplayAttribEXT() not found" was the case) never reaches
20
+ // a GLib handler. So fd 1 and fd 2 themselves are pointed at pipes that a reader thread
21
+ // forwards to logcat line by line.
22
+
23
+ #include "common.h"
24
+
25
+ #ifdef __ANDROID__
26
+
27
+ #include <android/log.h>
28
+ #include <errno.h>
29
+ #include <fcntl.h>
30
+ #include <sys/stat.h>
31
+ #include <unistd.h>
32
+
33
+ #include <cstdio>
34
+ #include <mutex>
35
+ #include <string>
36
+ #include <thread>
37
+
38
+ namespace nodegi {
39
+
40
+ // Whether `fd` is the /dev/null an app process starts with. Anything else (a terminal
41
+ // under `adb shell`, a file or pipe a host or the launcher set up) is a sink somebody
42
+ // chose, and taking it over would hide output from them.
43
+ static bool IsDevNull(int fd) {
44
+ struct stat fd_st, null_st;
45
+ return fstat(fd, &fd_st) == 0 && stat("/dev/null", &null_st) == 0 && S_ISCHR(fd_st.st_mode) &&
46
+ fd_st.st_rdev == null_st.st_rdev;
47
+ }
48
+
49
+ // Longest line forwarded whole; logcat truncates an entry near 4 KiB anyway, and a
50
+ // writer that never sends '\n' must not grow the buffer without bound.
51
+ static const size_t kMaxLine = 4000;
52
+
53
+ struct StdioPipe {
54
+ int read_fd;
55
+ int priority;
56
+ const char* tag;
57
+ };
58
+
59
+ static void ForwardLoop(StdioPipe pipe_info) {
60
+ std::string line;
61
+ char buf[1024];
62
+ for (;;) {
63
+ ssize_t n = read(pipe_info.read_fd, buf, sizeof buf);
64
+ if (n < 0 && errno == EINTR) continue;
65
+ if (n <= 0) break;
66
+ for (ssize_t i = 0; i < n; i++) {
67
+ if (buf[i] == '\n') {
68
+ __android_log_write(pipe_info.priority, pipe_info.tag, line.c_str());
69
+ line.clear();
70
+ } else {
71
+ line.push_back(buf[i]);
72
+ if (line.size() >= kMaxLine) {
73
+ __android_log_write(pipe_info.priority, pipe_info.tag, line.c_str());
74
+ line.clear();
75
+ }
76
+ }
77
+ }
78
+ }
79
+ if (!line.empty()) __android_log_write(pipe_info.priority, pipe_info.tag, line.c_str());
80
+ }
81
+
82
+ static void RedirectFd(int fd, int priority, const char* tag) {
83
+ if (!IsDevNull(fd)) return;
84
+ int fds[2];
85
+ if (pipe2(fds, O_CLOEXEC) != 0) return;
86
+ if (dup2(fds[1], fd) < 0) {
87
+ close(fds[0]);
88
+ close(fds[1]);
89
+ return;
90
+ }
91
+ close(fds[1]);
92
+ // Two pipes rather than one: priority is per entry, and stderr must stay WARN so a
93
+ // logcat filter on `*:W` still shows it.
94
+ std::thread(ForwardLoop, StdioPipe{fds[0], priority, tag}).detach();
95
+ }
96
+
97
+ // Once per process: Init runs once per env, but fd 1/2 are process-wide.
98
+ //
99
+ // Limit: abort() ends the process, and the reader thread with it. stderr is made
100
+ // unbuffered so the message at least reaches the pipe before the abort, but whether the
101
+ // reader has forwarded it by then is a race the tombstone still backs up.
102
+ static void InstallStdioRedirect() {
103
+ static std::once_flag once;
104
+ std::call_once(once, [] {
105
+ RedirectFd(STDOUT_FILENO, ANDROID_LOG_INFO, "stdout");
106
+ RedirectFd(STDERR_FILENO, ANDROID_LOG_WARN, "stderr");
107
+ setvbuf(stdout, nullptr, _IOLBF, 0);
108
+ setvbuf(stderr, nullptr, _IONBF, 0);
109
+ });
110
+ }
111
+
112
+ static void AndroidPrintHandler(const char* message) {
113
+ __android_log_print(ANDROID_LOG_INFO, "print", "%s", message);
114
+ }
115
+
116
+ static void AndroidPrinterrHandler(const char* message) {
117
+ __android_log_print(ANDROID_LOG_WARN, "print", "%s", message);
118
+ }
119
+
120
+ static int AndroidPriority(GLogLevelFlags log_level) {
121
+ if (log_level & G_LOG_LEVEL_ERROR) return ANDROID_LOG_FATAL;
122
+ if (log_level & G_LOG_LEVEL_CRITICAL) return ANDROID_LOG_ERROR;
123
+ if (log_level & G_LOG_LEVEL_WARNING) return ANDROID_LOG_WARN;
124
+ if (log_level & G_LOG_LEVEL_MESSAGE) return ANDROID_LOG_INFO;
125
+ if (log_level & G_LOG_LEVEL_INFO) return ANDROID_LOG_INFO;
126
+ if (log_level & G_LOG_LEVEL_DEBUG) return ANDROID_LOG_DEBUG;
127
+ return ANDROID_LOG_WARN;
128
+ }
129
+
130
+ static void AndroidLogHandler(const gchar* log_domain, GLogLevelFlags log_level,
131
+ const gchar* message, gpointer) {
132
+ __android_log_print(AndroidPriority(log_level),
133
+ log_domain != nullptr ? log_domain : "**", "%s", message);
134
+ }
135
+
136
+ static GLogWriterOutput AndroidLogWriter(GLogLevelFlags log_level, const GLogField* fields,
137
+ gsize n_fields, gpointer) {
138
+ const gchar* domain = nullptr;
139
+ const gchar* message = nullptr;
140
+ for (gsize i = 0; (domain == nullptr || message == nullptr) && i < n_fields; i++) {
141
+ if (g_strcmp0(fields[i].key, "GLIB_DOMAIN") == 0)
142
+ domain = static_cast<const gchar*>(fields[i].value);
143
+ else if (g_strcmp0(fields[i].key, "MESSAGE") == 0)
144
+ message = static_cast<const gchar*>(fields[i].value);
145
+ }
146
+ // liblog answers with the bytes it wrote, or a negative errno: anything above zero is
147
+ // a delivery. Reporting UNHANDLED on one sends GLib to its default writer, i.e. to the
148
+ // stderr this whole file exists because of.
149
+ int rc = __android_log_print(AndroidPriority(log_level),
150
+ domain != nullptr ? domain : "**", "%s",
151
+ message != nullptr ? message : "(empty)");
152
+ return rc > 0 ? G_LOG_WRITER_HANDLED : G_LOG_WRITER_UNHANDLED;
153
+ }
154
+
155
+ // androidLogBridge() -> boolean — whether GLib's messages reach logcat.
156
+ static Napi::Value AndroidLogBridge(const Napi::CallbackInfo& info) {
157
+ return Napi::Boolean::New(info.Env(), true);
158
+ }
159
+
160
+ void InitAndroidLog(Napi::Env env, Napi::Object exports) {
161
+ g_set_print_handler(AndroidPrintHandler);
162
+ g_set_printerr_handler(AndroidPrinterrHandler);
163
+ g_log_set_default_handler(AndroidLogHandler, nullptr);
164
+ g_log_set_writer_func(AndroidLogWriter, nullptr, nullptr);
165
+ InstallStdioRedirect();
166
+ exports.Set("androidLogBridge", Napi::Function::New(env, AndroidLogBridge));
167
+ }
168
+
169
+ } // namespace nodegi
170
+
171
+ #else
172
+
173
+ namespace nodegi {
174
+
175
+ static Napi::Value AndroidLogBridge(const Napi::CallbackInfo& info) {
176
+ return Napi::Boolean::New(info.Env(), false);
177
+ }
178
+
179
+ // Off Android stderr is a real sink and GLib's own handlers are the right ones; the
180
+ // export stays so a caller can ask without branching on the platform first.
181
+ void InitAndroidLog(Napi::Env env, Napi::Object exports) {
182
+ exports.Set("androidLogBridge", Napi::Function::New(env, AndroidLogBridge));
183
+ }
184
+
185
+ } // namespace nodegi
186
+
187
+ #endif
package/src/common.h CHANGED
@@ -19,11 +19,28 @@
19
19
  #ifndef NODE_GI_SRC_COMMON_H_
20
20
  #define NODE_GI_SRC_COMMON_H_
21
21
 
22
+ // NODE_GI_HAS_LIBUV — may this build REFERENCE libuv at all? Not "is there an
23
+ // event loop": bionic resolves every symbol at dlopen, so a single
24
+ // uv_timer_start left in the object file makes the whole addon fail to load on
25
+ // Android, whatever the call site does at runtime. The Android host is
26
+ // NativeScript's V8 runtime, a Node-API host with no libuv of its own
27
+ // (napi_get_uv_event_loop always fails there), so the uv bridge in loop.cc is
28
+ // compiled out and the ALooper pump beside it takes over.
29
+ #ifndef NODE_GI_HAS_LIBUV
30
+ #ifdef __ANDROID__
31
+ #define NODE_GI_HAS_LIBUV 0
32
+ #else
33
+ #define NODE_GI_HAS_LIBUV 1
34
+ #endif
35
+ #endif
36
+
22
37
  #ifndef _WIN32
23
38
  #include <dlfcn.h> // dlopen/dlsym the GtkWidgetClass template API (no GTK link)
24
39
  #endif
25
40
  #include <napi.h>
41
+ #if NODE_GI_HAS_LIBUV
26
42
  #include <uv.h>
43
+ #endif
27
44
 
28
45
  #include <girepository/girepository.h>
29
46
  #include <girepository/girffi.h> // gi_callable_info_create_closure + ffi_cif
@@ -178,6 +195,12 @@ const ForeignStructOps* ForeignOpsForInfo(GIBaseInfo* iface);
178
195
  void InitCairo(Napi::Env env, Napi::Object exports);
179
196
  // win32: probeHostOpenGL / preloadOpenGL / currentGLStrings (opengl-win32.cc); a no-op elsewhere.
180
197
  void InitOpenGLWin32(Napi::Env env, Napi::Object exports);
198
+ // Android: install GLib's print/log/writer handlers onto logcat (android-log.cc), where
199
+ // stderr goes nowhere. Elsewhere only the `androidLogBridge` probe, answering false.
200
+ void InitAndroidLog(Napi::Env env, Napi::Object exports);
201
+ // Android: androidInitGdk(), the opt-in GDK bring-up that replaces GTK's
202
+ // RuntimeApplication (android-gdk.cc). Elsewhere the export answers false.
203
+ void InitAndroidGdk(Napi::Env env, Napi::Object exports);
181
204
 
182
205
  // ---- GJS-exact 64-bit integer marshalling helpers (shared) -------------------
183
206
  //