Skip to content
This repository was archived by the owner on Dec 5, 2019. It is now read-only.

[WIP] Node bindings #68

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open

Conversation

paulyoung
Copy link
Collaborator

This relates to #33. Just getting things set up using the info from https://nodejs.org/api/addons.html

This creates a Node.js Addon in C++ with a function that gets re-exported by ES6 JavaScript and then tested using Jest.

@paulyoung
Copy link
Collaborator Author

FWIW, I tried adding flow but it kept failing on the import for the .node file despite having provided a declaration file for it.

@jspahrsummers
Copy link
Member

Thanks for doing this! I briefly looked into it not long after writing that issue, and got scared off by all the steps involved, so I'm very grateful. 😅

@paulyoung
Copy link
Collaborator Author

No problem, I don't blame you!

I actually forgot about my original idea for this which was to create the node bindings as a very thin wrapper (just the .cc files) and put the idiomatic JavaScript in bindings/js.

That way, other AltJS languages like PureScript purescript/purescript#631 (comment) can have bindings without the overhead of the JS layer.

@paulyoung
Copy link
Collaborator Author

I've added the beginning of some bindings according to the Node Addons guide. The extent of my C++ knowledge comes from there, so I'd appreciate any feedback on this so far.

@paulyoung
Copy link
Collaborator Author

Also, I'm getting a test failure that I don't quite understand so would appreciate another set of eyes on that:

 FAIL  __tests__/Version-test.js
  ✕ Version (9ms)

  ● Version

    expect(received).toBe(expected)

    Expected value to be (using ===):
      "alpha.0"
    Received:
      "dailybuild"

@paulyoung
Copy link
Collaborator Author

Additionally I'm unsure how to deal with calling ArbiterFree in the destructor.

This has become redundant given the direction that the node bindings
have taken.

This could be ressurected should a more modern API be desired (e.g.
property getters) but it seems like a lot of overhead at the moment.
@kastiglione
Copy link
Contributor

kastiglione commented Sep 26, 2016

I don't know anything about Node, but maybe I can ask the right questions or be a rubber duck.

I take it you're saying' ~SemanticVersion() can't just call ArbiterFree(_semanticVersion)? If so, why is that?

@paulyoung
Copy link
Collaborator Author

@kastiglione when I added #include <arbiter/Types.h> and did what you proposed I got this error:

../src/Version.cpp:35:3: error: no matching function for call to 'ArbiterFree'
  ArbiterFree(_semanticVersion);
  ^~~~~~~~~~~
../../../include/arbiter/Types.h:15:6: note: candidate function not viable: cannot convert argument of incomplete type
      'const ArbiterSemanticVersion *' to 'void *'
void ArbiterFree (void *object);
     ^
1 error generated.

In the methods I defined, I have to do this little dance:

Isolate *isolate = args.GetIsolate();
SemanticVersion *obj = ObjectWrap::Unwrap<SemanticVersion>(args.Holder());
const char *buildMetadata = ArbiterGetBuildMetadata(obj->_semanticVersion);

Notice that I have to do objc->_semanticVersion. I'm not sure how to do this in the destructor since it has no args

@paulyoung
Copy link
Collaborator Author

Somehow the build metadata is being returned when calling getPrereleaseVersion(), and I can't figure out why 🙈

if (buildMetadata == NULL) {
args.GetReturnValue().Set(Null(isolate));
} else {
args.GetReturnValue().Set(String::NewFromUtf8(isolate, buildMetadata));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this definitely copy the string?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo, good call.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say this ¯_(ツ)_/¯

Allocates a new string from UTF-8 data.

@paulyoung
Copy link
Collaborator Author

Given the new operator nonsense and the lack of proper support for overloadable constructors in JavaScript I'm going to use the factory pattern described in the addon guide instead.

This will be closer to how the C functions work anyway, and I should be able to use the shared examples I set up to test both.

@paulyoung
Copy link
Collaborator Author

I took a stab at that and added createSemanticVersionFromString but I get a segfault only when the string contains build metadata.

I verified this manually in case it was something invalid coming from the arbitrary string instances.

I imagine this is related to the other test failure.

} else {
const char *string = *String::Utf8Value(args[0]->ToString());
obj = new SemanticVersion(string);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel great about this but wasn't sure how else to support multiple constructors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine—and very JavaScripty—to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hindsight it sort of makes sense since JavaScript classes only support a single constructor.

@paulyoung
Copy link
Collaborator Author

I've got to be overlooking some memory management here, so any help is appreciated 🙇

_semanticVersion = ArbiterCreateSemanticVersionFromString(string);
}

SemanticVersion::~SemanticVersion() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use ArbiterFree() here, like @kastiglione suggested, as long as you store the semantic version pointer as non-const (which is correct, since you own it), and explicitly cast here with static_cast<void *>(…).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-const was what I was missing when I tried this earlier.

I did this in 8d7960d but didn't need to cast.

@jspahrsummers – do you know if that would be caught at compile time if it's an issue?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be caught, yeah. I'm surprised that it's not an issue, since C++ doesn't have implicit casting to void *.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe it just doesn't have implicit casting from void *

void SemanticVersion::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "SemanticVersion"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this sort of stuff have to be defined in C++, or is it possible to write more of it in JS? (It'd be nice if as much was JS as possible, since that's what users will see.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see—this is referring to SemanticVersion::New, not some magic symbol. In that case, nevermind, since there isn't much which avoids the C++ class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I wish there was a way to do more of this in JS, but I don't think there is.

This is definitely not what I thought I was getting in for in the beginning 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related: I've been going back and forth on whether it's worth providing ES6 classes that have property getters (think computed properties in Swift).

That way things like version.major would work instead of version.getMajorversion(), but I'm not sure if the overhead outweighs the benefit:

  • we'd need to subclass or create thin wrappers around everything exported from the native bindings, which may introduce more potential for bugs
  • we may need to transpile to ES5 if wanting to target older versions of Node

That was the motiviation behind 9a46594.

I think it could be a nicer API so will give it another shot once these bugs are worked out.

void SemanticVersion::Create(const FunctionCallbackInfo<Value>& args) {
Isolate *isolate = args.GetIsolate();
const unsigned argc = 5;
Local<Value> argv[argc] = { args[0], args[1], args[2], args[3], args[4] };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on out-of-bounds here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it just blindly accesses invalid memory (classic C[++] style). That might explain the test failure you see.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on out-of-bounds here?

Nothing right now, I'll address that.

Copy link
Collaborator Author

@paulyoung paulyoung Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this but it didn't make any difference:

View diff

-  Local<Value> argv[argc] = { args[0], args[1], args[2], args[3], args[4] };
+
+  Local<Value> argv[argc] = {
+    args[0],
+    args[1],
+    args[2],
+    (args[3]->IsNull() || args[3]->IsUndefined() ? (Local<Value>)Null(isolate) : args[3]),
+    (args[4]->IsNull() || args[4]->IsUndefined() ? (Local<Value>)Null(isolate) : args[4])
+  };

Copy link
Collaborator Author

@paulyoung paulyoung Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was an old diff.

View diff

-  Local<Value> argv[argc] = { args[0], args[1], args[2], args[3], args[4] };
+
+  Local<Value> argv[argc] = {
+    args[0],
+    args[1],
+    args[2],
+    (args.Length() > 3 && (args[3]->IsNull() || args[3]->IsUndefined()) ? (Local<Value>)Null(isolate) : args[3]),
+    (args.Length() > 4 && (args[4]->IsNull() || args[4]->IsUndefined()) ? (Local<Value>)Null(isolate) : args[4])

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That logic is wrong.

Copy link
Collaborator Author

@paulyoung paulyoung Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-  Local<Value> argv[argc] = { args[0], args[1], args[2], args[3], args[4] };
+
+  Local<Value> argv[argc] = {
+    args[0],
+    args[1],
+    args[2],
+    (args.Length() <= 3 || args[3]->IsNull() || args[3]->IsUndefined() ? (Local<Value>)Null(isolate) : args[3]),
+    (args.Length() <= 4 || args[4]->IsNull() || args[4]->IsUndefined() ? (Local<Value>)Null(isolate) : args[4])

@paulyoung
Copy link
Collaborator Author

I'm going to take another stab at this soon but welcome any and all ideas in the meantime 😄

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants