koffi 1.1.5 → 1.2.0-alpha.3

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.
package/src/call.hh CHANGED
@@ -23,7 +23,11 @@ namespace RG {
23
23
 
24
24
  bool AnalyseFunction(InstanceData *instance, FunctionInfo *func);
25
25
 
26
- class CallData {
26
+ struct BackRegisters;
27
+
28
+ // I'm not sure why the alignas(8), because alignof(CallData) is 8 without it.
29
+ // But on Windows i386, without it, the alignment may not be correct (compiler bug?).
30
+ class alignas(8) CallData {
27
31
  struct OutObject {
28
32
  Napi::ObjectReference ref;
29
33
  const uint8_t *ptr;
@@ -37,9 +41,12 @@ class CallData {
37
41
  InstanceMemory *mem;
38
42
  Span<uint8_t> old_stack_mem;
39
43
  Span<uint8_t> old_heap_mem;
44
+ uint32_t used_trampolines = 0;
40
45
 
41
46
  LocalArray<OutObject, MaxOutParameters> out_objects;
42
- uint8_t *sp;
47
+
48
+ uint8_t *new_sp;
49
+ uint8_t *old_sp;
43
50
 
44
51
  union {
45
52
  uint32_t u32;
@@ -61,6 +68,8 @@ public:
61
68
  void Execute();
62
69
  Napi::Value Complete();
63
70
 
71
+ void Relay(Size idx, uint8_t *own_sp, uint8_t *caller_sp, BackRegisters *out_reg);
72
+
64
73
  void DumpDebug() const;
65
74
 
66
75
  private:
@@ -77,6 +86,8 @@ private:
77
86
  void PopObject(Napi::Object obj, const uint8_t *src, const TypeInfo *type, int16_t realign = 0);
78
87
  Napi::Object PopObject(const uint8_t *src, const TypeInfo *type, int16_t realign = 0);
79
88
  Napi::Value PopArray(const uint8_t *src, const TypeInfo *type, int16_t realign = 0);
89
+
90
+ Size ReserveTrampoline(const FunctionInfo *proto, Napi::Function func);
80
91
  };
81
92
 
82
93
  template <typename T>
@@ -85,7 +96,8 @@ inline bool CallData::AllocStack(Size size, Size align, T **out_ptr)
85
96
  uint8_t *ptr = AlignDown(mem->stack.end() - size, align);
86
97
  Size delta = mem->stack.end() - ptr;
87
98
 
88
- if (RG_UNLIKELY(mem->stack.len < delta)) {
99
+ // Keep 512 bytes for redzone (required in some ABIs)
100
+ if (RG_UNLIKELY(mem->stack.len - 512 < delta)) {
89
101
  ThrowError<Napi::Error>(env, "FFI call is taking up too much memory");
90
102
  return false;
91
103
  }
@@ -122,4 +134,6 @@ inline bool CallData::AllocHeap(Size size, Size align, T **out_ptr)
122
134
  return true;
123
135
  }
124
136
 
137
+ void *GetTrampoline(Size idx, const FunctionInfo *proto);
138
+
125
139
  }
package/src/ffi.cc CHANGED
@@ -40,11 +40,6 @@
40
40
 
41
41
  namespace RG {
42
42
 
43
- const Size SyncStackSize = Mebibytes(2);
44
- const Size SyncHeapSize = Mebibytes(4);
45
- const Size AsyncStackSize = Mebibytes(1);
46
- const Size AsyncHeapSize = Mebibytes(2);
47
-
48
43
  // Value does not matter, the tag system uses memory addresses
49
44
  const int TypeInfoMarker = 0xDEADBEEF;
50
45
 
@@ -325,6 +320,140 @@ static Napi::Value CreateArrayType(const Napi::CallbackInfo &info)
325
320
  return external;
326
321
  }
327
322
 
323
+ static bool ParseClassicFunction(Napi::Env env, Napi::String name, Napi::Value ret,
324
+ Napi::Array parameters, FunctionInfo *func)
325
+ {
326
+ InstanceData *instance = env.GetInstanceData<InstanceData>();
327
+
328
+ #ifdef _WIN32
329
+ if (!name.IsString() && !name.IsNumber()) {
330
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for name, expected string or integer", GetValueType(instance, name));
331
+ return false;
332
+ }
333
+ #else
334
+ if (!name.IsString()) {
335
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for name, expected string", GetValueType(instance, name));
336
+ return false;
337
+ }
338
+ #endif
339
+
340
+ func->name = DuplicateString(name.ToString().Utf8Value().c_str(), &instance->str_alloc).ptr;
341
+
342
+ func->ret.type = ResolveType(instance, ret);
343
+ if (!func->ret.type)
344
+ return false;
345
+ if (func->ret.type->primitive == PrimitiveKind::Array) {
346
+ ThrowError<Napi::Error>(env, "You are not allowed to directly return fixed-size arrays");
347
+ return false;
348
+ }
349
+
350
+ if (!parameters.IsArray()) {
351
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for parameters of '%2', expected an array", GetValueType(instance, parameters), func->name);
352
+ return false;
353
+ }
354
+
355
+ uint32_t parameters_len = parameters.Length();
356
+
357
+ if (parameters_len) {
358
+ Napi::String str = ((Napi::Value)parameters[parameters_len - 1]).As<Napi::String>();
359
+
360
+ if (str.IsString() && str.Utf8Value() == "...") {
361
+ func->variadic = true;
362
+ parameters_len--;
363
+ }
364
+ }
365
+
366
+ for (uint32_t j = 0; j < parameters_len; j++) {
367
+ ParameterInfo param = {};
368
+
369
+ param.type = ResolveType(instance, parameters[j], &param.directions);
370
+ if (!param.type)
371
+ return false;
372
+ if (param.type->primitive == PrimitiveKind::Void ||
373
+ param.type->primitive == PrimitiveKind::Array) {
374
+ ThrowError<Napi::TypeError>(env, "Type %1 cannot be used as a parameter", param.type->name);
375
+ return false;
376
+ }
377
+
378
+ if (func->parameters.len >= MaxParameters) {
379
+ ThrowError<Napi::TypeError>(env, "Functions cannot have more than %1 parameters", MaxParameters);
380
+ return false;
381
+ }
382
+ if ((param.directions & 2) && ++func->out_parameters >= MaxOutParameters) {
383
+ ThrowError<Napi::TypeError>(env, "Functions cannot have more than out %1 parameters", MaxOutParameters);
384
+ return false;
385
+ }
386
+
387
+ param.offset = (int8_t)j;
388
+
389
+ func->parameters.Append(param);
390
+ }
391
+
392
+ return true;
393
+ }
394
+
395
+ static Napi::Value CreateCallbackType(const Napi::CallbackInfo &info)
396
+ {
397
+ Napi::Env env = info.Env();
398
+ InstanceData *instance = env.GetInstanceData<InstanceData>();
399
+
400
+ FunctionInfo *func = instance->callbacks.AppendDefault();
401
+ RG_DEFER_N(err_guard) { instance->callbacks.RemoveLast(1); };
402
+
403
+ if (info.Length() >= 3) {
404
+ if (!ParseClassicFunction(env, info[0u].As<Napi::String>(), info[1u], info[2u].As<Napi::Array>(), func))
405
+ return env.Null();
406
+ } else if (info.Length() >= 1) {
407
+ if (!info[0].IsString()) {
408
+ ThrowError<Napi::TypeError>(env, "Unexpected %1 value for prototype, expected string", GetValueType(instance, info[0]));
409
+ return env.Null();
410
+ }
411
+
412
+ std::string proto = info[0u].As<Napi::String>();
413
+ if (!ParsePrototype(env, proto.c_str(), func))
414
+ return env.Null();
415
+ } else {
416
+ ThrowError<Napi::TypeError>(env, "Expected 1 or 3 arguments, not %1", info.Length());
417
+ return env.Null();
418
+ }
419
+
420
+ if (func->variadic) {
421
+ LogError("Variadic callbacks are not supported");
422
+ return env.Null();
423
+ }
424
+ if (func->convention != CallConvention::Cdecl &&
425
+ func->convention != CallConvention::Stdcall) {
426
+ ThrowError<Napi::Error>(env, "Only Cdecl and Stdcall callbacks are supported");
427
+ return env.Null();
428
+ }
429
+
430
+ if (!AnalyseFunction(instance, func))
431
+ return env.Null();
432
+
433
+ // We cannot fail after this check
434
+ if (instance->types_map.Find(func->name)) {
435
+ ThrowError<Napi::Error>(env, "Duplicate type name '%1'", func->name);
436
+ return env.Null();
437
+ }
438
+ err_guard.Disable();
439
+
440
+ TypeInfo *type = instance->types.AppendDefault();
441
+
442
+ type->name = func->name;
443
+
444
+ type->primitive = PrimitiveKind::Callback;
445
+ type->align = alignof(void *);
446
+ type->size = RG_SIZE(void *);
447
+ type->proto = func;
448
+
449
+ instance->types_map.Set(type);
450
+
451
+ Napi::External<TypeInfo> external = Napi::External<TypeInfo>::New(env, type);
452
+ SetValueTag(instance, external, &TypeInfoMarker);
453
+
454
+ return external;
455
+ }
456
+
328
457
  static Napi::Value GetTypeSize(const Napi::CallbackInfo &info)
329
458
  {
330
459
  Napi::Env env = info.Env();
@@ -380,7 +509,7 @@ static Napi::Value GetTypeDefinition(const Napi::CallbackInfo &info)
380
509
  return type->defn.Value();
381
510
  }
382
511
 
383
- static InstanceMemory *AllocateAsyncMemory(InstanceData *instance)
512
+ static InstanceMemory *AllocateMemory(InstanceData *instance)
384
513
  {
385
514
  for (Size i = 1; i < instance->memories.len; i++) {
386
515
  InstanceMemory *mem = instance->memories[i];
@@ -391,7 +520,7 @@ static InstanceMemory *AllocateAsyncMemory(InstanceData *instance)
391
520
 
392
521
  InstanceMemory *mem = new InstanceMemory();
393
522
 
394
- mem->stack.len = AsyncStackSize;
523
+ mem->stack.len = StackSize;
395
524
  #if defined(_WIN32)
396
525
  mem->stack.ptr = (uint8_t *)VirtualAlloc(nullptr, mem->stack.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
397
526
  #elif defined(__APPLE__)
@@ -401,7 +530,7 @@ static InstanceMemory *AllocateAsyncMemory(InstanceData *instance)
401
530
  #endif
402
531
  RG_CRITICAL(mem->stack.ptr, "Failed to allocate %1 of memory", mem->stack.len);
403
532
 
404
- mem->heap.len = AsyncHeapSize;
533
+ mem->heap.len = HeapSize;
405
534
  #ifdef _WIN32
406
535
  mem->heap.ptr = (uint8_t *)VirtualAlloc(nullptr, mem->heap.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
407
536
  #else
@@ -489,7 +618,7 @@ static Napi::Value TranslateVariadicCall(const Napi::CallbackInfo &info)
489
618
  }
490
619
 
491
620
  param.variadic = true;
492
- param.offset = i + 1;
621
+ param.offset = (int8_t)(i + 1);
493
622
 
494
623
  func.parameters.Append(param);
495
624
  }
@@ -581,7 +710,7 @@ static Napi::Value TranslateAsyncCall(const Napi::CallbackInfo &info)
581
710
  return env.Null();
582
711
  }
583
712
 
584
- InstanceMemory *mem = AllocateAsyncMemory(instance);
713
+ InstanceMemory *mem = AllocateMemory(instance);
585
714
  AsyncCall *async = new AsyncCall(env, instance, func, mem, callback);
586
715
 
587
716
  if (async->Prepare(info) && instance->debug) {
@@ -592,78 +721,6 @@ static Napi::Value TranslateAsyncCall(const Napi::CallbackInfo &info)
592
721
  return env.Null();
593
722
  }
594
723
 
595
- static bool ParseClassicFunction(Napi::Env env, Napi::String name, Napi::Value ret,
596
- Napi::Array parameters, FunctionInfo *func)
597
- {
598
- InstanceData *instance = env.GetInstanceData<InstanceData>();
599
-
600
- #ifdef _WIN32
601
- if (!name.IsString() && !name.IsNumber()) {
602
- ThrowError<Napi::TypeError>(env, "Unexpected %1 value for name, expected string or integer", GetValueType(instance, name));
603
- return false;
604
- }
605
- #else
606
- if (!name.IsString()) {
607
- ThrowError<Napi::TypeError>(env, "Unexpected %1 value for name, expected string", GetValueType(instance, name));
608
- return false;
609
- }
610
- #endif
611
-
612
- func->name = DuplicateString(name.ToString().Utf8Value().c_str(), &instance->str_alloc).ptr;
613
-
614
- func->ret.type = ResolveType(instance, ret);
615
- if (!func->ret.type)
616
- return false;
617
- if (func->ret.type->primitive == PrimitiveKind::Array) {
618
- ThrowError<Napi::Error>(env, "You are not allowed to directly return fixed-size arrays");
619
- return false;
620
- }
621
-
622
- if (!parameters.IsArray()) {
623
- ThrowError<Napi::TypeError>(env, "Unexpected %1 value for parameters of '%2', expected an array", GetValueType(instance, parameters), func->name);
624
- return false;
625
- }
626
-
627
- uint32_t parameters_len = parameters.Length();
628
-
629
- if (parameters_len) {
630
- Napi::String str = ((Napi::Value)parameters[parameters_len - 1]).As<Napi::String>();
631
-
632
- if (str.IsString() && str.Utf8Value() == "...") {
633
- func->variadic = true;
634
- parameters_len--;
635
- }
636
- }
637
-
638
- for (uint32_t j = 0; j < parameters_len; j++) {
639
- ParameterInfo param = {};
640
-
641
- param.type = ResolveType(instance, parameters[j], &param.directions);
642
- if (!param.type)
643
- return false;
644
- if (param.type->primitive == PrimitiveKind::Void ||
645
- param.type->primitive == PrimitiveKind::Array) {
646
- ThrowError<Napi::TypeError>(env, "Type %1 cannot be used as a parameter", param.type->name);
647
- return false;
648
- }
649
-
650
- if (func->parameters.len >= MaxParameters) {
651
- ThrowError<Napi::TypeError>(env, "Functions cannot have more than %1 parameters", MaxParameters);
652
- return false;
653
- }
654
- if ((param.directions & 2) && ++func->out_parameters >= MaxOutParameters) {
655
- ThrowError<Napi::TypeError>(env, "Functions cannot have more than out %1 parameters", MaxOutParameters);
656
- return false;
657
- }
658
-
659
- param.offset = j;
660
-
661
- func->parameters.Append(param);
662
- }
663
-
664
- return true;
665
- }
666
-
667
724
  static Napi::Value FindLibraryFunction(const Napi::CallbackInfo &info, CallConvention convention)
668
725
  {
669
726
  Napi::Env env = info.Env();
@@ -977,27 +1034,8 @@ InstanceMemory::~InstanceMemory()
977
1034
 
978
1035
  InstanceData::InstanceData()
979
1036
  {
980
- InstanceMemory *mem = new InstanceMemory();
981
-
982
- mem->stack.len = SyncStackSize;
983
- #if defined(_WIN32)
984
- mem->stack.ptr = (uint8_t *)VirtualAlloc(nullptr, mem->stack.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
985
- #elif defined(__APPLE__)
986
- mem->stack.ptr = (uint8_t *)mmap(nullptr, mem->stack.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
987
- #else
988
- mem->stack.ptr = (uint8_t *)mmap(nullptr, mem->stack.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_STACK, -1, 0);
989
- #endif
990
- RG_CRITICAL(mem->stack.ptr, "Failed to allocate %1 of memory", mem->stack.len);
991
-
992
- mem->heap.len = SyncHeapSize;
993
- #ifdef _WIN32
994
- mem->heap.ptr = (uint8_t *)VirtualAlloc(nullptr, mem->heap.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
995
- #else
996
- mem->heap.ptr = (uint8_t *)mmap(nullptr, mem->heap.len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
997
- #endif
998
- RG_CRITICAL(mem->heap.ptr, "Failed to allocate %1 of memory", mem->heap.len);
999
-
1000
- memories.Append(mem);
1037
+ AllocateMemory(this);
1038
+ RG_ASSERT(memories.len == 1);
1001
1039
  }
1002
1040
 
1003
1041
  InstanceData::~InstanceData()
@@ -1015,6 +1053,7 @@ static void SetExports(Napi::Env env, Func func)
1015
1053
  func("handle", Napi::Function::New(env, CreateHandleType));
1016
1054
  func("pointer", Napi::Function::New(env, CreatePointerType));
1017
1055
  func("array", Napi::Function::New(env, CreateArrayType));
1056
+ func("callback", Napi::Function::New(env, CreateCallbackType));
1018
1057
  func("sizeof", Napi::Function::New(env, GetTypeSize));
1019
1058
  func("alignof", Napi::Function::New(env, GetTypeAlign));
1020
1059
  func("introspect", Napi::Function::New(env, GetTypeDefinition));
package/src/ffi.hh CHANGED
@@ -19,8 +19,12 @@
19
19
 
20
20
  namespace RG {
21
21
 
22
+ static const Size StackSize = Mebibytes(1);
23
+ static const Size HeapSize = Mebibytes(2);
24
+
22
25
  static const Size MaxParameters = 32;
23
26
  static const Size MaxOutParameters = 8;
27
+ static const Size MaxTrampolines = 16;
24
28
 
25
29
  extern const int TypeInfoMarker;
26
30
 
@@ -41,7 +45,8 @@ enum class PrimitiveKind {
41
45
  Record,
42
46
  Array,
43
47
  Float32,
44
- Float64
48
+ Float64,
49
+ Callback
45
50
  };
46
51
  static const char *const PrimitiveKindNames[] = {
47
52
  "Void",
@@ -60,11 +65,13 @@ static const char *const PrimitiveKindNames[] = {
60
65
  "Record",
61
66
  "Array",
62
67
  "Float32",
63
- "Float64"
68
+ "Float64",
69
+ "Callback"
64
70
  };
65
71
 
66
72
  struct TypeInfo;
67
73
  struct RecordMember;
74
+ struct FunctionInfo;
68
75
 
69
76
  struct TypeInfo {
70
77
  enum class ArrayHint {
@@ -85,6 +92,7 @@ struct TypeInfo {
85
92
  HeapArray<RecordMember> members; // Record only
86
93
  const TypeInfo *ref; // Pointer or array
87
94
  ArrayHint hint; // Array only
95
+ const FunctionInfo *proto; // Callback only
88
96
 
89
97
  RG_HASHTABLE_HANDLER(TypeInfo, name);
90
98
  };
@@ -123,7 +131,7 @@ struct ParameterInfo {
123
131
  const TypeInfo *type;
124
132
  int directions;
125
133
  bool variadic;
126
- Size offset;
134
+ int8_t offset;
127
135
 
128
136
  // ABI-specific part
129
137
 
@@ -149,11 +157,12 @@ struct ParameterInfo {
149
157
  #endif
150
158
  };
151
159
 
160
+ // Also used for callbacks, even though many members are not used in this case
152
161
  struct FunctionInfo {
153
162
  mutable std::atomic_int refcount {1};
154
163
 
155
164
  const char *name;
156
- const char *decorated_name;
165
+ const char *decorated_name; // Only set for some platforms/calling conventions
157
166
  const LibraryHolder *lib = nullptr;
158
167
 
159
168
  void *func;
@@ -161,7 +170,7 @@ struct FunctionInfo {
161
170
 
162
171
  ParameterInfo ret;
163
172
  HeapArray<ParameterInfo> parameters;
164
- Size out_parameters;
173
+ int8_t out_parameters;
165
174
  bool variadic;
166
175
 
167
176
  // ABI-specific part
@@ -190,19 +199,29 @@ struct InstanceMemory {
190
199
  bool temporary;
191
200
  };
192
201
 
202
+ struct TrampolineInfo {
203
+ const FunctionInfo *proto;
204
+ Napi::Function func;
205
+ };
206
+
193
207
  struct InstanceData {
194
208
  InstanceData();
195
209
  ~InstanceData();
196
210
 
197
211
  BucketArray<TypeInfo> types;
198
212
  HashTable<const char *, TypeInfo *> types_map;
213
+ BucketArray<FunctionInfo> callbacks;
199
214
 
200
215
  bool debug;
201
216
  uint64_t tag_lower;
202
217
 
203
218
  LocalArray<InstanceMemory *, 6> memories;
204
219
 
220
+ TrampolineInfo trampolines[MaxTrampolines];
221
+ uint32_t free_trampolines = UINT32_MAX;
222
+
205
223
  BlockAllocator str_alloc;
206
224
  };
225
+ RG_STATIC_ASSERT(MaxTrampolines <= 32);
207
226
 
208
227
  }
package/src/parser.cc CHANGED
@@ -87,7 +87,7 @@ bool PrototypeParser::Parse(const char *str, FunctionInfo *out_func)
87
87
  return false;
88
88
  }
89
89
 
90
- param.offset = out_func->parameters.len;
90
+ param.offset = (int8_t)out_func->parameters.len;
91
91
 
92
92
  out_func->parameters.Append(param);
93
93
 
package/test/misc.c CHANGED
@@ -120,6 +120,10 @@ typedef struct FixedWide {
120
120
  int16_t buf[64];
121
121
  } FixedWide;
122
122
 
123
+ typedef struct SingleU32 { uint32_t v; } SingleU32;
124
+ typedef struct SingleU64 { uint64_t v; } SingleU64;
125
+ typedef struct SingleI64 { int64_t v; } SingleI64;
126
+
123
127
  EXPORT void FillPack1(int a, Pack1 *p)
124
128
  {
125
129
  p->a = a;
@@ -410,3 +414,73 @@ EXPORT FixedWide ReturnFixedWide(FixedWide str)
410
414
  {
411
415
  return str;
412
416
  }
417
+
418
+ EXPORT uint32_t ThroughUInt32UU(uint32_t v)
419
+ {
420
+ return v;
421
+ }
422
+ EXPORT SingleU32 ThroughUInt32SS(SingleU32 s)
423
+ {
424
+ return s;
425
+ }
426
+ EXPORT SingleU32 ThroughUInt32SU(uint32_t v)
427
+ {
428
+ SingleU32 s;
429
+ s.v = v;
430
+ return s;
431
+ }
432
+ EXPORT uint32_t ThroughUInt32US(SingleU32 s)
433
+ {
434
+ return s.v;
435
+ }
436
+
437
+ EXPORT uint64_t ThroughUInt64UU(uint64_t v)
438
+ {
439
+ return v;
440
+ }
441
+ EXPORT SingleU64 ThroughUInt64SS(SingleU64 s)
442
+ {
443
+ return s;
444
+ }
445
+ EXPORT SingleU64 ThroughUInt64SU(uint64_t v)
446
+ {
447
+ SingleU64 s;
448
+ s.v = v;
449
+ return s;
450
+ }
451
+ EXPORT uint64_t ThroughUInt64US(SingleU64 s)
452
+ {
453
+ return s.v;
454
+ }
455
+
456
+ EXPORT int64_t ThroughInt64II(int64_t v)
457
+ {
458
+ return v;
459
+ }
460
+ EXPORT SingleI64 ThroughInt64SS(SingleI64 s)
461
+ {
462
+ return s;
463
+ }
464
+ EXPORT SingleI64 ThroughInt64SI(int64_t v)
465
+ {
466
+ SingleI64 s;
467
+ s.v = v;
468
+ return s;
469
+ }
470
+ EXPORT int64_t ThroughInt64IS(SingleI64 s)
471
+ {
472
+ return s.v;
473
+ }
474
+
475
+ EXPORT float CallSimpleJS(int i, float (*func)(int i, const char *str, double d))
476
+ {
477
+ float f = func(i, "Hello!", 42.0);
478
+ return f;
479
+ }
480
+
481
+ EXPORT int TransferToJS(const char *str, int (*cb)(const char *str))
482
+ {
483
+ char buf[64];
484
+ snprintf(buf, sizeof(buf), "Hello %s!", str);
485
+ return cb(buf);
486
+ }