highlight.js

Thursday, January 25, 2024

Cue Club 2 on Apple Silicon

It's been quite a long time for me not to play a decent snooker game since I replaced my Windows laptop with an Apple Silicon one. To be more specific, the Cue Club 2 on Steam is the one. However, it only supports Windows. In this article, I'll share you how to set up this game on Apple Silicon.

Environment

  • MacBook Pro M2 Max
  • 64GB RAM

1. Install VMware Fusion

VMware Fusion is the Desktop Hypervisors for Mac with all-new Windows 11 support on Macs with Apple silicon. It offers a free version of personal use license. Just visit the official website to install it.

Apply for a personal use license.

2. Install Windows 11

Follow the official instructions to install Windows 11 arm64.

3. Install Steam

Visit the official website, install the Steam, and install the game. Both Steam and the game are installed to C:\Program Files (x86)\Steam, though the Windows runs on arm64.

4. Graphics Settings

Click the button [Play] to launch the game. Here is my preference on the screen resolution for your reference.

It's better not to turn on the VMware Fusion full screen mode, because the game may crash.

Enjoy the Game




Friday, September 8, 2023

Undefined Symbols in V8 Monolith

There are many applications and libraries acting as embedders to Google V8 JavaScript engine. My open source project Javet is one of them. When I was about to upgrade to V8 v11.7, the build was broken. I'd like to share the troubleshooting story in this post with you.

Undefined symbols absl::time_internal::cctz::local_time_zone()

The Javet build on V8 v11.7 worked well on Windows, Linux and Android, but failed on Mac x86_64 and arm64 with the following cmake error logs.

Undefined symbols for architecture x86_64:
  "_CFRelease", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetCString", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetLength", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetMaximumSizeForEncoding", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFTimeZoneCopyDefault", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFTimeZoneGetName", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
ld: symbol(s) not found for architecture x86_64

Undefined symbols for architecture arm64:
  "_CFRelease", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetCString", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetLength", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFStringGetMaximumSizeForEncoding", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFTimeZoneCopyDefault", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
  "_CFTimeZoneGetName", referenced from:
      absl::time_internal::cctz::local_time_zone() in libv8_monolith.a(time_zone_lookup.o)
ld: symbol(s) not found for architecture arm64

Analysis

V8 has a build target called v8_monolith which is for embedders. It usually has all the symbols built in libv8_monolith.a. However it forgets to include Abseil - C++ Common Libraries as the error log shows undefined symbols during the link phase.

So, the goal is to find or build those missing symbols for the linker to work properly.

Solution

After going over the official Abseil doc, I found the solution at Abseil CMake Build Instructions. Basically, I needed to add Abseil to the Javet CMakeList.txt so that the linker won't complain anymore.

add_subdirectory(${V8_DIR}/third_party/abseil-cpp ${V8_RELEASE_DIR}/third_party/abseil-cpp)
target_link_libraries(Javet PUBLIC absl::base absl::time)
  • ${V8_DIR} is the directory of the V8 source code.
  • ${V8_RELEASE_DIR} is the directory of the build target.
    • x86_64: ${V8_DIR}/out.gn/x64.release
    • arm64: ${V8_DIR}/out.gn/arm64.release

After this patch was applied, the build and test passed.

Thursday, May 25, 2023

CAPEA_KPEAVIsolate or CUPEA_KPEAVIsolate?

I tried to build V8 monolith on Windows 10 for v11.4 and v11.5 recently. The builds were good. However, when I tried to build my app linking v8_monolith.lib, I got the following error messages.

javet_converter.obj : error LNK2019: unresolved external symbol "private: static unsigned __int64 * __cdecl v8::internal::HandleScope::Extend(class v8::internal::Isolate *)" (?Extend @HandleScope@internal@v8@@CAPEA_KPEAVIsolate@23@@Z) referenced in function "class _jobject * __cdecl Javet::Converter::ToExternalV8Value(struct JNIEnv_ *,class Javet::V8Runtime const  *,class v8::Local<class v8::Context> const &,class v8::internal::Object const &)" (?ToExternalV8Value@Converter@Javet@@YAPEAV_jobject@@PEAUJNIEnv_@@PEBVV8Runtime@2@AEBV?$Local@VCont ext@v8@@@v8@@AEBVObject@internal@7@@Z)

I located the symbols via dumpbin and found the highlighted one actually is CUPEA_KPEAVIsolate.

I'm not sure what caused the corrupted V8 build. I fixed it by binary searching and replacing CUPEA_KPEAVIsolate with CAPEA_KPEAVIsolate, and it works.

Tuesday, July 5, 2022

Serialize and Deserialize PageImpl in Jackson

It's very common to put paginated query results in Redis via Jackson serialization and deserialization. However, org.springframework.data.domain.PageImpl doesn't expose a default constructor. Even worse, that applies to org.springframework.data.domain.Sort and org.springframework.data.domain.Sort.Order as well. There are a couple of workarounds. I'd like to introduce a less intrusive approach for your reference.

The idea is to write a Jackson module which injects a custom serializer and deserializer for org.springframework.data.domain.PageImpl. Let's see how to do that.

1. Define an interface.

public interface IJPADataPage {
String _CLASS = "@class";
String CONTENT = "content";
String DIRECTION = "direction";
String IGNORE_CASE = "ignoreCase";
String NULL_HANDLING = "nullHandling";
String NUMBER = "number";
String PROPERTY = "property";
String SIZE = "size";
String SORT = "sort";
String TOTAL_ELEMENTS = "totalElements";
}

2. Define a Serializer.

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Sort;

import java.io.IOException;

@SuppressWarnings("unchecked")
public class JPADataPageSerializer extends StdSerializer<PageImpl> implements IJPADataPage {
public JPADataPageSerializer(Class<PageImpl> t) {
super(t);
}

public JPADataPageSerializer() {
this(null);
}

@Override
public void serialize(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(_CLASS, PageImpl.class.getName());
jsonGenerator.writePOJOField(CONTENT, value.getContent());
jsonGenerator.writeNumberField(NUMBER, value.getNumber());
jsonGenerator.writeNumberField(SIZE, value.getSize());
jsonGenerator.writeNumberField(TOTAL_ELEMENTS, value.getTotalElements());
jsonGenerator.writeArrayFieldStart(SORT);
if (!value.getSort().isEmpty()) {
for (Sort.Order order : value.getSort()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(PROPERTY, order.getProperty());
jsonGenerator.writeStringField(DIRECTION, order.getDirection().name());
jsonGenerator.writeBooleanField(IGNORE_CASE, order.isIgnoreCase());
jsonGenerator.writeStringField(NULL_HANDLING, order.getNullHandling().name());
jsonGenerator.writeEndObject();
}
}
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}

@Override
public void serializeWithType(
PageImpl value,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider,
TypeSerializer typeSer) throws IOException {
serialize(value, jsonGenerator, serializerProvider);
}
}

3. Define a Deserializer.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("unchecked")
public class JPADataPageDeserializer extends StdDeserializer<PageImpl> implements IJPADataPage {
public JPADataPageDeserializer(Class<?> type) {
super(type);
}

public JPADataPageDeserializer() {
this(null);
}

@Override
public PageImpl deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
List<?> content = deserializationContext.readTreeAsValue(jsonNode.get(CONTENT), List.class);
int number = jsonNode.get(NUMBER).asInt(0);
int size = jsonNode.get(SIZE).asInt(1);
int totalElements = jsonNode.get(TOTAL_ELEMENTS).asInt(0);
List<Sort.Order> orders = new ArrayList<>();
ArrayNode arrayNode = (ArrayNode) jsonNode.get(SORT);
if (!arrayNode.isEmpty()) {
for (JsonNode jsonNodeOrder : arrayNode) {
String property = jsonNodeOrder.get(PROPERTY).asText();
Sort.Direction direction = Sort.Direction.valueOf(jsonNodeOrder.get(DIRECTION).asText());
boolean ignoreCase = jsonNodeOrder.get(IGNORE_CASE).asBoolean();
Sort.NullHandling nullHandling = Sort.NullHandling.valueOf(jsonNodeOrder.get(NULL_HANDLING).asText());
Sort.Order order = new Sort.Order(direction, property, nullHandling);
if (ignoreCase) {
order = order.ignoreCase();
}
orders.add(order);
}
}
PageRequest pageRequest = PageRequest.of(number, size, Sort.by(orders));
return new PageImpl(content, pageRequest, totalElements);
}

@Override
public Object deserializeWithType(
JsonParser jsonParser,
DeserializationContext deserializationContext,
TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jsonParser, deserializationContext);
}
}

4. Define a module.

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.data.domain.PageImpl;

public class JPADataModule extends SimpleModule {
public static final String NAME = "JPA Data Module";
public static final Version VERSION = new Version(0, 1, 0, null, null, null);

public JPADataModule() {
super(NAME, VERSION);
addSerializer(PageImpl.class, new JPADataPageSerializer());
addDeserializer(PageImpl.class, new JPADataPageDeserializer());
}
}

5. Register the module.

objectMapper.registerModule(new JPADataModule());

Now Jackson is able to handle PageImpl and the Redis cache works.

Saturday, April 30, 2022

IDEA / HandBrake / WSL Port Conflict

Recently I upgraded HandBrake to the latest version, however, it stopped working. The root cause is TCP port conflict. As usual, I ran net stop winnat && net start winnat and it worked. But, that broke WSL network.

I didn't want to reset the winnat every time I started Windows with a broken WSL. Finally, I found the root cause: Hyper-V reserves huge amount of TCP ports after Windows is up. The fix is to tell Hyper-V to avoid those commonly used TCP ports.

The following command can show you the excluded TCP port ranges.

> netsh int ipv4 show excludedportrange protocol=tcp

Protocol tcp Port Exclusion Ranges

Start Port    End Port
----------    --------
        80          80
      1000        1010
      1020        1030
      ......

As you can see, huge amount of TCP ports are reserved by Hyper-V. So, let's tell Hyper-V not to be that greedy by executing the following command.

> netsh int ipv4 set dynamic tcp start=49152 num=16384

That command tells Windows to allow dynamic TCP ports from 49152 so that Hyper-V is only able to reserve TCP ports starting from 49152. The following command can verify the setting is correct.

> netsh int ipv4 show dynamic protocol=tcp

Protocol tcp Dynamic Port Range
---------------------------------
Start Port      : 49152
Number of Ports : 16384

Once the new dynamic TCP port range is set, just reboot your machine and everything goes back to normal. IDEA can start smoothly, WSL network is always on, HandBrake works all the time.

Tuesday, January 11, 2022

dlopen failed: cannot locate symbol "__aarch64_ldadd4_relax"

In Android development, it's rare to meet the following error.

dlopen failed: cannot locate symbol "__aarch64_ldadd4_relax"

I searched the whole internet for a solution, but couldn't get a practical one. Actually, the root cause is simple: The Android NDK is too old.

The solution in my case is:

  1. Upgrade CMake to the latest version.
  2. Upgrade Android NDK to the latest version.

Wednesday, November 3, 2021

Monday, October 25, 2021

Javet for Android is Released

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

It's been more than half a year for the Javet users to wait for the Android support. Now, Javet has officially supported Android.

The API and coding experience are identical to the ones on Linux, Mac OS and Windows.




Friday, October 22, 2021

Javet for Android is on the Way

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

It's been a long while for many Javet users to wait for the Android support. Now, it is coming true as the first Android build is being tested. If you are interested, please join us at discord.



Friday, September 10, 2021

MacBook Air mid-2012 from Lion to Catalina

I had been asked by potential Javet users for Mac OS release for many moths before the first Mac OS release was published. I have to admit I have no plan to purchase Mac OS devices in the new future for financial reason.

Luckily, I have a MacBook Air mid-2012 resting in the dust. I revived it and would like to upgrade it to the latest Mac OS.


The problem was I had to upgrade it to Lion, then to El Capitan, Mojave, eventually to Catalina which is the last version supported by MacBook Air mid-2012.


It took ~8 hours to get there. Then, I installed xcode and related tools. Luckily, it met the lowest requirement for building V8 v9.2.

Obviously, MacBook Air mid-2012 is tooooo slow in building modern applications. It took ~4 hours to build V8, and another ~4 hours to build Node.js. The result was good as Javet for Mac OS x86_64 was built successfully.

However, MacBook Air mid-2012 will soon been retired by a new release of V8. When that day comes, I will no longer release Mac OS x86_64 version unless I get enough donation for me to purchase a new device.

Migrate from J2V8 to Javet

How to migrate from J2V8 to Javet is a frequently asked question, especially when people are evaluating Javet. I created Javet in Jan, 2021 for various reasons (What is the Motivation?, History with J2V8). After the first release v0.7.0 was published, I started migrating from J2V8 to Javet. It was quite smooth, though it took a week.

Why Migrate from J2V8 to Javet?

  • Its Linux, Mac OS and Windows releases have been abandoned for years.

  • Its type hierarchy is inconsistent because primitive types are out of the hierarchy so that tedious if-else sentences have to be repeated all over the code base.

  • Its function registration API is kind of verbose.

  • Segfaults take place so frequently and don't get maintainers' attention for years.

  • Its locking mechanism heavily increases mental pressure in the code base.

  • Its V8 runtime is not multi-threaded friendly unless application adds a synchronous layer on top of it.

Migration Guides

V8 ⟶ V8Runtime

  • V8 in J2V8 is V8Runtime in Javet.

  • V8 in J2V8 carries 2 roles: 1 as the V8 runtime and 1 as the global object (globalThis or global). In Javet, V8Runtime no longer inherits from V8Value so that it literally represents the V8 runtime. V8Runtime.getGlobalObject() is dedicated to the global object.

  • V8Runtime has much richer API than V8 has. E.g. compileV8Module(), lowMemoryNotification(), terminateExecution().

Primitive Types

  • Primitive types in Javet inherit from V8ValuePrimitiveV8ValueV8Data.

  • The Javet type hierarchy is consistent so that V8Value in all supported API can represent all V8 types. This is hard in J2V8 because Object has to be used to represent all types, however, by using Object the type check during compilation doesn't work at all and that is a rich source of runtime bugs or even segfaults.

registerJavaMethod() ⟶ @V8Function

  • It is quite painful to register many functions in J2V8. Javet makes that a declarative one instead of the imperative one. Just decorate the target function with @V8Function, then call V8ValueObject.bind(javaObject) to bind that Java object, it's done.

  • In addition, Javet provides @V8Property which allows registering getters and setters in the same manner. That feature has never been delivered by J2V8.

  • Javet also allows unbinding the registration. Just call V8ValueObject.unbind(javaObject).

Please refer to V8 Function for more details.

V8Locker

  • Javet introduced Implicit Mode which allows applications to eliminate V8Locker from the code base and still be able to share the same V8Runtime among multiple threads, because Javet does the synchronization automatically. That frees applications developers from the tedious acquire() and release() calls, and gets the rid of the runtime exceptions caused by multiple threads.

  • Javet also has Explicit Mode for performance sensitive scenarios.

Please refer to Know the Lock for more details.

Type Conversion

  • Javet has built-in JavetObjectConverter which covers the majority cases on type conversion so that the arguments of Javet API can be of any type and the converter just does the conversion transparently. That frees application developers from writing tedious type conversion code everywhere.

  • Javet also provides JavetProxyConverter which allows injecting arbitrary Java objects in V8 and polyfilling Java interfaces with JavaScript functions or objects. Especially the polyfilling feature implies hotfixing business logic without restarting the JVM.

Please refer to Object Converter for more details.

Node.js and V8

  • Javet provides both Node.js mode and V8 mode for various usages. Each mode stays at a dedicated classloader so that both modes don't cross each other, and are completely isolated. If the application only uses one mode, it doesn't need to pay extra amount of memory for the other mode because the other mode is not loaded at all. Of course, both modes can be unloaded as well without shutting down the JVM.

  • In Node.js mode, all node modules can be directly used including the native modules. Please refer to Modularization for more detail.

  • In V8 mode, it is much more secure than the Node.js mode is, but lacks of some basic ES API, e.g. setTimeout(). Project Javenode is the one that aims at simulating Node.js with Java in Javet V8 mode.

Please refer to Javet Design for more details.

ES6 Module

  • Javet supports import { *** } from '***.js' and exposes module resolve event for applications to specify where to locate the modules.

Please refer to Modularization for more detail.

Blessing

In case this migration guide couldn't cover all your use cases, please contact the maintainer at discord. Wish you a successful migration!

Monday, September 6, 2021

Javet v0.9.11 is released with some exciting features

Javet v0.9.11 is released with some exciting features.

  • Mac OS (x86_64)
  • Node.js v14.17.6
  • V8 v9.3.345.16
  • Getter/Setter by Symbol
  • Dynamic Proxy for Java Objects by JavaScript Objects
  • Promise

Thursday, September 2, 2021

How to Efficiently Wait for RxJava Observer to Complete?

Almost all tutorials including the RxJava official ones deliver a message to RxJava developers that Thread.sleep(...) is recommended in testing RxJava code snippets. However, that is not efficient enough.

Here is an alternative way for your reference.
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
ExecutorService executorService = Executors.newFixedThreadPool(4);
Observable.timer(10, TimeUnit.MILLISECONDS, Schedulers.from(executorService))
        .subscribe(t -> atomicBoolean.set(true));
executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
assertTrue(atomicBoolean.get());
The key is to inject a custom thread pool and wait for that thread pool to complete.

Wednesday, September 1, 2021

V8 v9.3 and Mac OS: error: no member named 'forward' in namespace 'std'

As Chrome v93 is stable, I began to upgrade Javet to V8 v9.3.345.16. However, the build failed on my MacBook Air with the following error messages.

../../include/cppgc/allocation.h:168:39: error: no member named 'forward' in namespace 'std'
    T* object = ::new (memory) T(std::forward<Args>(args)...);   
                                 ~~~~~^                
../../include/cppgc/allocation.h:168:47: error: 'Args' does not refer to a value
    T* object = ::new (memory) T(std::forward<Args>(args)...);
                                              ^

Well, as std::forward is missing, the fix is simple. Just add #include <utility> into include/cppgc/allocation.h and it works.

Monday, August 16, 2021

Javet Supports Mac OS Now

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

I'm very happy to announce Javet supports Mac OS x86_64 from v0.9.9.

Maven

<dependency>
    <groupId>com.caoccao.javet</groupId>
    <artifactId>javet-macos</artifactId>
    <version>0.9.9</version>
</dependency>

Gradle Kotlin DSL

implementation("com.caoccao.javet:javet-macos:0.9.9")

Gradle Groovy DSL

implementation 'com.caoccao.javet:javet-macos:0.9.9'

Hello Javet

// Node.js Mode
try (V8Runtime v8Runtime = V8Host.getNodeInstance().createV8Runtime()) {
    System.out.println(v8Runtime.getExecutor("'Hello Javet'").executeString());
}

// V8 Mode
try (V8Runtime v8Runtime = V8Host.getV8Instance().createV8Runtime()) {
    System.out.println(v8Runtime.getExecutor("'Hello Javet'").executeString());
}

Wednesday, August 11, 2021

JNI Symbol Conflicts in Mac OS

Background

When I was adding Mac OS support to my open source project Javet, I found a weird behavior caused by JNI symbol conflicts which resulted in JVM 8 core-dump.

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java. It's embedding behavior relies on 2 JNI libraries one for Node.js and one for V8. Both of them are loaded by dedicated classloader and share the same API and symbols, except that the Node.js one exposes more Node.js flavored symbols.

In Javet unit test, it loads both Node.js and V8 libraries and performs complicated test. The test goes well in Linux and Windows, but core-dumps in Mac OS.

Analysis

The dump file shows the call stack crosses the library boundary.
...node...dylib
...node...dylib
...node...dylib <== Something wrong happens here.
...v8...dylib
...v8...dylib
...v8...dylib
It seems JVM in Mac OS registers global symbols in the same memory space, so that the V8 library calls into Node.js library. Obviously, that for sure leads to core-dump.

Failed Attempts

  • I tried various distributions of JDK 8. None of them worked. It seems to be the Mac OS' behavior.
  • I tried to adjust the CMakeLists.txt. All my attempts failed.

Fix

I kept reviewing the call stack and realized the issue might be gone if these symbols were not global symbols.

So I created jni/exported_symbols_list.txt with the following content.
_JNI_OnLoad
_JNI_OnUnload
_Java_com_caoccao_javet_*
_napi_*
Then, I updated CMakeLists.txt with the following content.
target_link_libraries(Javet PUBLIC -exported_symbols_list ${CMAKE_SOURCE_DIR}/jni/exported_symbols_list.txt)
And, it works, no more core-dump with all test cases pass.

Monday, August 2, 2021

Javet - Java and JavaScript Interop

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

From v0.9.8, Javet allows injecting arbitrary Java objects into V8 which enables the complete interop between Java and JavaScript. To enable this feature, application just needs to call v8Runtime.setConverter(new JavetProxyConverter());. Here are 3 examples.

Inject a Static Class

v8Runtime.getGlobalObject().set("System", System.class);
v8Runtime.getExecutor("function main() {\n" +
        // Java reference can be directly called in JavaScript.
        "  System.out.println('Hello from Java');\n" +
        // Java reference can be directly assigned to JavaScript variable.
        "  const println = System.out.println;\n" +
        // Java reference can be directly assigned to JavaScript variable.
        "  println('Hello from JavaScript');\n" +
        "}\n" +
        "main();").executeVoid();
v8Runtime.getGlobalObject().delete("System");

/*
 * Output:
 *   Hello from Java
 *   Hello from JavaScript
 */

Inject an Enum

v8Runtime.getGlobalObject().set("Color", Color.class);
System.out.println(v8Runtime.getExecutor("Color.pink.toString();").executeString());
System.out.println("The enum in JavaScript is the one in Java: " +
        (Color.pink == (Color) v8Runtime.getExecutor("Color.pink;").executeObject()));
v8Runtime.getGlobalObject().delete("Color");

/*
 * Output:
 *   java.awt.Color[r=255,g=175,b=175]
 *   The enum in JavaScript is the one in Java: true
 */

Inject a Pattern

Pattern pattern = Pattern.compile("^\\d+$");
v8Runtime.getExecutor("function main(pattern) {\n" +
        "  return [\n" +
        "    pattern.matcher('123').matches(),\n" +
        "    pattern.matcher('abc').matches(),\n" +
        "  ];\n" +
        "}").executeVoid();
System.out.println(v8Runtime.getGlobalObject().invokeObject("main", pattern).toString());

/*
 * Output:
 *   [true, false]
 */

Inject a StringBuilder

v8Runtime.getGlobalObject().set("StringBuilder", StringBuilder.class);
 System.out.println(v8Runtime.getExecutor("function main() {\n" +
         "  return new StringBuilder('Hello').append(' from StringBuilder').toString();\n" +
         "}\n" +
         "main();").executeString());
 v8Runtime.getGlobalObject().delete("StringBuilder");

/*
 * Output:
 *   Hello from StringBuilder
 */

Thursday, July 29, 2021

Peking Opera Blues: Pure Delirium

Early today I was watching the newly (in 2020) released extras in a 2-disc BD and was shocked by your 18'25 story with Peking Opera Blues. At the end of it, I found a link to this blog post. What an amazing thing from watching BD extras.



Wednesday, May 19, 2021

Javet - Patch V8 Function at Source Code Level

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

With the release of v0.8.8, Javet supports patching V8 function at source code level on the fly.

Why is That Important?

Functions can be changed on the fly at JavaScript code level via Javet API. Why to choose this approach? Because sometimes local scoped context is required which is usually called closure. E.g:

const a = function () {
    const b = 1;
    return () => b;
}
const x = a();
console.log(x());
// Output is: 1

Local const b is visible to the anonymous function at line 3, but invisible to the function interceptor. Javet provides a way of changing the function at JavaScript source code level so that local scoped context is still visible.

How?

getSourceCode() and setSourceCode(String sourceCode) are designed for getting and setting the source code. setSourceCode(String sourceCode) actually performs the follow steps.

def setSourceCode(sourceCode):
    existingSourceCode = v8Function.getSourceCode()
    (startPosition, endPosition) = v8Function.getPosition()
    newSourceCode = existingSourceCode[:startPosition] + sourceCode + existingSourceCode[endPosition:]
    v8Function.setSourceCode(newSourceCode)
    v8Function.setPosition(startPosition, startPosition + len(sourceCode))

Be careful, setSourceCode(String sourceCode) has radical impacts that may break the execution because all functions during one execution share the same source code but have their own positions. The following diagram shows the rough memory layout. Assuming function (4) has been changed to something else with position changed, function (1) and (2) will not be impacted because their positions remain the same, but function (3) will be broken because its end position is not changed to the end position of function (4) accordingly.


Javet does not scan memory for all impacted function. So, it is caller's responsibility for restoring the original source code after invocation. The pseudo logic is as following.

originalSourceCode = v8ValueFunction.getSourceCode()
v8ValueFunction.setSourceCode(sourceCode)
v8ValueFunction.call(...)
v8ValueFunction.setSourceCode(originalSourceCode)

Why does setSourceCode() sometimes return false? Usually, that means the local scoped context hasn't been generated by V8. getJSScopeType().isClass() == true indicates that state. After callVoid(null), the local scoped context will be created with getJSScopeType().isFunction() == true and setSourceCode() will work. The pseudo logic is as following.

originalSourceCode = v8ValueFunction.getSourceCode()
if (v8ValueFunction.getJSScopeType().isClass()) {
    try {
        v8ValueFunction.callVoid(null);
        // Now v8ValueFunction.getJSScopeType().isFunction() is true
    } catch (JavetException e) {
    }
}
v8ValueFunction.setSourceCode(sourceCode) // true
v8ValueFunction.call(...)
v8ValueFunction.setSourceCode(originalSourceCode)

The rough lifecycle of a V8 function is as following.


What is the Source Code of a Function in V8?

When V8 calculates start position of a function, it does not include the keyword function and function name. E.g.

function abc(a, b, c) { ... } // Source code is (a, b, c) { ... }

(a, b, c) => { ... }          // Source code is (a, b, c) => { ... }

So, please always discard the keyword function and function name when calling setSourceCode().

Monday, May 10, 2021

Javet — An awesome way of embedding Node.js and V8 in Java

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.

In Oct, 2020, I was looking for a solution about running JavaScript code in JVM with identical behavior as in a web browser. V8 seemed to be only candidate and the available solution was J2V8.

However, J2V8 has dropped support for Linux in 2017 and Windows in 2016. The V8 doesn’t even completely support ES6.

In Jan, 2021, I started an open-source project https://github.com/caoccao/Javet/ from scratch. Now, it is fully functional and reaches production level.

Major Features

  • 🐧Linux + 🖥️Windows

Quick Start

Dependency

Maven

<dependency>
<groupId>com.caoccao.javet</groupId>
<artifactId>javet</artifactId>
<version>0.8.7</version>
</dependency>

Gradle Kotlin DSL

implementation("com.caoccao.javet:javet:0.8.7")

Gradle Groovy DSL

implementation 'com.caoccao.javet:javet:0.8.7'

Hello Javet

// Node.js Mode
try (V8Runtime v8Runtime = V8Host.getNodeInstance().createV8Runtime()) {
System.out.println(v8Runtime.getExecutor("'Hello Javet'").executeString());
}
// V8 Mode
try (V8Runtime v8Runtime = V8Host.getV8Instance().createV8Runtime()) {
System.out.println(v8Runtime.getExecutor("'Hello Javet'").executeString());
}

Cue Club 2 on Apple Silicon

It's been quite a long time for me not to play a decent snooker game since I replaced my Windows laptop with an Apple Silicon one. To be...