Skip to main content

V8 Data Types and Conversion

Mastering data type conversion between JavaScript and C++ is essential for building efficient addons. This chapter covers all V8 data types, conversion patterns, and performance optimization.

V8 Type Hierarchy

Understanding V8 Types

Value (base class)
├── Primitive
│ ├── Undefined
│ ├── Null
│ ├── Boolean
│ ├── String
│ ├── Symbol
│ └── Number
├── Object
│ ├── Array
│ ├── Function
│ ├── Date
│ ├── RegExp
│ ├── Promise
│ ├── Map
│ ├── Set
│ └── TypedArray
└── External (wraps C++ pointers)

Key principle: All JavaScript values in C++ are represented as Local<Value>. You must cast to specific types after type checking.

Working with Numbers

Creating Numbers

numbers.cc:

#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Integer;
using v8::Value;

// Return an integer
void GetInteger(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// Create integer
Local<Integer> result = Integer::New(isolate, 42);
args.GetReturnValue().Set(result);
}

// Return a floating-point number
void GetDouble(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// Create number
Local<Number> result = Number::New(isolate, 3.14159);
args.GetReturnValue().Set(result);
}

// Return large integer (int64)
void GetLargeInt(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// JavaScript Number can safely represent integers up to 2^53-1
int64_t largeNumber = 9007199254740991LL; // Number.MAX_SAFE_INTEGER

Local<Number> result = Number::New(isolate, static_cast<double>(largeNumber));
args.GetReturnValue().Set(result);
}

} // namespace demo

Converting Numbers from JavaScript

convert-numbers.cc:

void ProcessNumber(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

if (args.Length() < 1 || !args[0]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected a number").ToLocalChecked()));
return;
}

Local<Value> arg = args[0];

// Various number conversions
double asDouble = arg->NumberValue(context).FromMaybe(0.0);
int32_t asInt32 = arg->Int32Value(context).FromMaybe(0);
uint32_t asUint32 = arg->Uint32Value(context).FromMaybe(0);
int64_t asInt64 = arg->IntegerValue(context).FromMaybe(0);

// Use the values
std::cout << "Double: " << asDouble << std::endl;
std::cout << "Int32: " << asInt32 << std::endl;
std::cout << "Uint32: " << asUint32 << std::endl;
std::cout << "Int64: " << asInt64 << std::endl;

args.GetReturnValue().Set(asDouble * 2);
}

JavaScript usage:

const addon = require('./build/Release/addon');

console.log(addon.getInteger()); // 42
console.log(addon.getDouble()); // 3.14159
console.log(addon.getLargeInt()); // 9007199254740991

addon.processNumber(42); // Logs conversions, returns 84
addon.processNumber(3.7); // Handles floating point
addon.processNumber(-100); // Handles negatives

Important: JavaScript numbers are always 64-bit floating point (IEEE 754). Integers are safe up to ±2^53-1.

Working with Strings

Creating Strings

strings.cc:

void CreateString(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// From C string literal
Local<String> str1 = String::NewFromUtf8(
isolate,
"Hello World"
).ToLocalChecked();

// From std::string
std::string cppString = "C++ String";
Local<String> str2 = String::NewFromUtf8(
isolate,
cppString.c_str()
).ToLocalChecked();

// With explicit length
const char* data = "Test\0Hidden"; // Contains null byte
Local<String> str3 = String::NewFromUtf8(
isolate,
data,
NewStringType::kNormal,
11 // Explicit length includes null byte
).ToLocalChecked();

args.GetReturnValue().Set(str1);
}

// Concatenate strings
void ConcatStrings(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected two strings").ToLocalChecked()));
return;
}

// Convert to C++ strings
String::Utf8Value str1(isolate, args[0]);
String::Utf8Value str2(isolate, args[1]);

// Concatenate in C++
std::string result = std::string(*str1) + std::string(*str2);

// Convert back to V8 string
args.GetReturnValue().Set(
String::NewFromUtf8(isolate, result.c_str()).ToLocalChecked());
}

String Conversion Patterns

Pattern 1: Quick conversion (stack-allocated)

void ProcessString(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

String::Utf8Value str(isolate, args[0]);

// Use as C string
const char* cstr = *str;
printf("String: %s\n", cstr);

// Get length
int length = str.length();
}

Pattern 2: std::string conversion

void ToStdString(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

String::Utf8Value utf8(isolate, args[0]);

// Convert to std::string (safer for complex operations)
std::string str(*utf8, utf8.length());

// Can now use std::string methods
std::transform(str.begin(), str.end(), str.begin(), ::toupper);

args.GetReturnValue().Set(
String::NewFromUtf8(isolate, str.c_str()).ToLocalChecked());
}

Pattern 3: Handling non-ASCII (Unicode)

void HandleUnicode(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

String::Utf8Value utf8(isolate, args[0]);

// UTF-8 encoded string
std::string str(*utf8, utf8.length());

// Length in bytes (not characters!)
size_t byteLength = str.length();

// For character count, need to parse UTF-8
// (Use a UTF-8 library like utf8cpp)
}

JavaScript usage:

addon.concatStrings("Hello ", "World");  // "Hello World"
addon.toStdString("hello"); // "HELLO"
addon.handleUnicode("Hello 世界"); // Handles Unicode correctly

Working with Booleans

booleans.cc:

void GetBoolean(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// Create boolean
Local<Boolean> trueValue = Boolean::New(isolate, true);
Local<Boolean> falseValue = Boolean::New(isolate, false);

args.GetReturnValue().Set(trueValue);
}

void ProcessBoolean(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

if (!args[0]->IsBoolean()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected boolean").ToLocalChecked()));
return;
}

// Convert to C++ bool
bool value = args[0]->BooleanValue(isolate);

if (value) {
std::cout << "Value is true" << std::endl;
} else {
std::cout << "Value is false" << std::endl;
}

args.GetReturnValue().Set(!value); // Return negated value
}

Truthy/falsy conversion:

// Any value can be converted to boolean
bool isTruthy = args[0]->BooleanValue(isolate);

// These are falsy: false, 0, "", null, undefined, NaN
// Everything else is truthy

Working with Arrays

Creating Arrays

arrays.cc:

void CreateArray(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

// Create array with length
Local<Array> arr = Array::New(isolate, 3);

// Set elements
arr->Set(context, 0, Number::New(isolate, 10)).Check();
arr->Set(context, 1, Number::New(isolate, 20)).Check();
arr->Set(context, 2, Number::New(isolate, 30)).Check();

args.GetReturnValue().Set(arr);
}

// Return array from C++ vector
void VectorToArray(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

// C++ vector
std::vector<int> numbers = {1, 2, 3, 4, 5};

// Create V8 array
Local<Array> arr = Array::New(isolate, numbers.size());

for (size_t i = 0; i < numbers.size(); i++) {
arr->Set(context, i, Number::New(isolate, numbers[i])).Check();
}

args.GetReturnValue().Set(arr);
}

Processing Arrays

process-array.cc:

void SumArray(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

if (!args[0]->IsArray()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected array").ToLocalChecked()));
return;
}

Local<Array> arr = args[0].As<Array>();
uint32_t length = arr->Length();

double sum = 0;

for (uint32_t i = 0; i < length; i++) {
Local<Value> element = arr->Get(context, i).ToLocalChecked();

if (element->IsNumber()) {
sum += element->NumberValue(context).FromMaybe(0.0);
}
}

args.GetReturnValue().Set(Number::New(isolate, sum));
}

// Convert array to C++ vector
void ArrayToVector(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

Local<Array> arr = args[0].As<Array>();
uint32_t length = arr->Length();

std::vector<double> numbers;
numbers.reserve(length); // Pre-allocate for efficiency

for (uint32_t i = 0; i < length; i++) {
Local<Value> element = arr->Get(context, i).ToLocalChecked();

if (element->IsNumber()) {
numbers.push_back(element->NumberValue(context).FromMaybe(0.0));
}
}

// Process vector...
double sum = std::accumulate(numbers.begin(), numbers.end(), 0.0);

args.GetReturnValue().Set(Number::New(isolate, sum));
}

JavaScript usage:

const arr = addon.createArray();      // [10, 20, 30]
const vec = addon.vectorToArray(); // [1, 2, 3, 4, 5]

const sum = addon.sumArray([1, 2, 3, 4, 5]); // 15

Working with Objects

Creating Objects

objects.cc:

void CreateObject(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

// Create empty object
Local<Object> obj = Object::New(isolate);

// Set properties
obj->Set(
context,
String::NewFromUtf8(isolate, "name").ToLocalChecked(),
String::NewFromUtf8(isolate, "John Doe").ToLocalChecked()
).Check();

obj->Set(
context,
String::NewFromUtf8(isolate, "age").ToLocalChecked(),
Number::New(isolate, 30)
).Check();

obj->Set(
context,
String::NewFromUtf8(isolate, "active").ToLocalChecked(),
Boolean::New(isolate, true)
).Check();

// Nested array
Local<Array> hobbies = Array::New(isolate, 2);
hobbies->Set(context, 0,
String::NewFromUtf8(isolate, "reading").ToLocalChecked()).Check();
hobbies->Set(context, 1,
String::NewFromUtf8(isolate, "coding").ToLocalChecked()).Check();

obj->Set(
context,
String::NewFromUtf8(isolate, "hobbies").ToLocalChecked(),
hobbies
).Check();

args.GetReturnValue().Set(obj);
}

Reading Object Properties

read-object.cc:

void ReadObjectProperties(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();

if (!args[0]->IsObject()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected object").ToLocalChecked()));
return;
}

Local<Object> obj = args[0].As<Object>();

// Get property
Local<Value> name = obj->Get(
context,
String::NewFromUtf8(isolate, "name").ToLocalChecked()
).ToLocalChecked();

if (name->IsString()) {
String::Utf8Value utf8(isolate, name);
std::cout << "Name: " << *utf8 << std::endl;
}

// Check if property exists
bool hasAge = obj->Has(
context,
String::NewFromUtf8(isolate, "age").ToLocalChecked()
).FromMaybe(false);

if (hasAge) {
Local<Value> age = obj->Get(
context,
String::NewFromUtf8(isolate, "age").ToLocalChecked()
).ToLocalChecked();

if (age->IsNumber()) {
double ageValue = age->NumberValue(context).FromMaybe(0);
std::cout << "Age: " << ageValue << std::endl;
}
}

// Get all property names
Local<Array> propertyNames = obj->GetPropertyNames(context).ToLocalChecked();
uint32_t length = propertyNames->Length();

std::cout << "Properties: ";
for (uint32_t i = 0; i < length; i++) {
Local<Value> key = propertyNames->Get(context, i).ToLocalChecked();
String::Utf8Value keyUtf8(isolate, key);
std::cout << *keyUtf8 << " ";
}
std::cout << std::endl;
}

JavaScript usage:

const user = addon.createObject();
// {
// name: 'John Doe',
// age: 30,
// active: true,
// hobbies: ['reading', 'coding']
// }

addon.readObjectProperties(user);
// Logs: Name: John Doe
// Age: 30
// Properties: name age active hobbies

Working with Buffers

Buffers are crucial for high-performance binary data processing.

buffers.cc:

void CreateBuffer(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

size_t length = 1024;

// Allocate buffer
Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(isolate, length);

// Get pointer to data
std::shared_ptr<v8::BackingStore> backing = buffer->GetBackingStore();
uint8_t* data = static_cast<uint8_t*>(backing->Data());

// Fill with data
for (size_t i = 0; i < length; i++) {
data[i] = i % 256;
}

args.GetReturnValue().Set(buffer);
}

void ProcessBuffer(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

if (!args[0]->IsArrayBuffer() && !args[0]->IsArrayBufferView()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected Buffer or TypedArray")
.ToLocalChecked()));
return;
}

Local<v8::ArrayBuffer> buffer;
size_t byte_offset = 0;
size_t byte_length = 0;

if (args[0]->IsArrayBufferView()) {
Local<v8::ArrayBufferView> view = args[0].As<v8::ArrayBufferView>();
buffer = view->Buffer();
byte_offset = view->ByteOffset();
byte_length = view->ByteLength();
} else {
buffer = args[0].As<v8::ArrayBuffer>();
byte_length = buffer->ByteLength();
}

std::shared_ptr<v8::BackingStore> backing = buffer->GetBackingStore();
uint8_t* data = static_cast<uint8_t*>(backing->Data()) + byte_offset;

// Process data
uint64_t sum = 0;
for (size_t i = 0; i < byte_length; i++) {
sum += data[i];
}

args.GetReturnValue().Set(Number::New(isolate, static_cast<double>(sum)));
}

// Efficient buffer copy (in-place modification)
void ModifyBuffer(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

Local<v8::ArrayBuffer> buffer = args[0].As<v8::ArrayBuffer>();
std::shared_ptr<v8::BackingStore> backing = buffer->GetBackingStore();
uint8_t* data = static_cast<uint8_t*>(backing->Data());
size_t length = backing->ByteLength();

// Modify in place (zero-copy)
for (size_t i = 0; i < length; i++) {
data[i] = data[i] * 2;
}

// No return needed - buffer modified in place
}

JavaScript usage:

const buffer = addon.createBuffer();        // ArrayBuffer of 1024 bytes
console.log(buffer.byteLength); // 1024

const sum = addon.processBuffer(buffer); // Sum all bytes

addon.modifyBuffer(buffer); // Doubles all values in-place

TypedArrays

typed-arrays.cc:

void ProcessTypedArray(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

if (!args[0]->IsTypedArray()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Expected TypedArray").ToLocalChecked()));
return;
}

Local<v8::TypedArray> typedArray = args[0].As<v8::TypedArray>();

// Get underlying ArrayBuffer
Local<v8::ArrayBuffer> buffer = typedArray->Buffer();
std::shared_ptr<v8::BackingStore> backing = buffer->GetBackingStore();

size_t byte_offset = typedArray->ByteOffset();
size_t byte_length = typedArray->ByteLength();
size_t length = typedArray->Length();

// Check specific type
if (typedArray->IsUint8Array()) {
uint8_t* data = static_cast<uint8_t*>(backing->Data()) + byte_offset;
// Process as uint8...
} else if (typedArray->IsFloat64Array()) {
double* data = reinterpret_cast<double*>(
static_cast<uint8_t*>(backing->Data()) + byte_offset);
// Process as double...
}
}

Null and Undefined

null-undefined.cc:

void ReturnUndefined(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(v8::Undefined(isolate));
}

void ReturnNull(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(v8::Null(isolate));
}

void CheckNullOrUndefined(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Value> val = args[0];

if (val->IsNull()) {
std::cout << "Value is null" << std::endl;
} else if (val->IsUndefined()) {
std::cout << "Value is undefined" << std::endl;
} else {
std::cout << "Value is defined" << std::endl;
}
}

Performance Optimization

Minimize Conversions

Bad: Multiple conversions

void ProcessString(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// Conversion 1
String::Utf8Value str1(isolate, args[0]);
std::string s1(*str1);

// Conversion 2 (unnecessary!)
String::Utf8Value str2(isolate, args[0]);
std::string s2(*str2);

// Use s1 and s2 (wasteful)
}

Good: Single conversion

void ProcessString(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

// Convert once
String::Utf8Value str(isolate, args[0]);
std::string s(*str);

// Reuse s
}

Reuse Objects

Bad: Create in loop

for (int i = 0; i < 1000; i++) {
Local<String> key = String::NewFromUtf8(isolate, "key").ToLocalChecked();
// Use key...
}

Good: Create once

Local<String> key = String::NewFromUtf8(isolate, "key").ToLocalChecked();
for (int i = 0; i < 1000; i++) {
// Reuse key
}

Batch Operations

Bad: Individual operations

for (int i = 0; i < array.size(); i++) {
Local<Number> num = Number::New(isolate, array[i]);
result->Set(context, i, num).Check();
}

Good: Batch when possible

// Process in C++ first
std::vector<double> processed = ProcessInCpp(array);

// Then convert to V8 objects
for (size_t i = 0; i < processed.size(); i++) {
result->Set(context, i, Number::New(isolate, processed[i])).Check();
}

Summary

You've mastered V8 data type conversion:

Key Takeaways:

  • Always check types before conversion (IsNumber(), IsString(), etc.)
  • Use ToLocalChecked() for infallible operations
  • Use FromMaybe() for operations that might fail
  • Buffers enable zero-copy data transfer
  • Minimize conversions for better performance
  • Reuse V8 objects when possible

Type Conversion Checklist:

  • ✓ Check type before converting
  • ✓ Handle conversion failures
  • ✓ Use appropriate C++ types
  • ✓ Minimize unnecessary conversions
  • ✓ Prefer in-place modifications for buffers

Next: Async operations with libuv