JaneaSystems / nodejs-mobile-samples

Repository for demo applications that use Node.js on Mobile
MIT License
168 stars 111 forks source link

Does this NodeJS Framework work with iOS Native apps using Swift ? #20

Open umangkathuria opened 4 years ago

umangkathuria commented 4 years ago

Is there a Sample that I could refer to incorporate this framework in iOS App using Swift. I am not familiar with ObjectiveC or C++. Could you please help me in getting started with Swift based native app ?

Thanks!

jaimecbernardo commented 4 years ago

Hi @umangkathuria ,

It's been used with Swift before: The start API has been introduced by the community in this PR: https://github.com/JaneaSystems/nodejs-mobile/pull/18

Unfortunately I'm not familiar enough with Swift to help you get started. Hopefully someone from the community give some input here.

tomholub commented 4 years ago

Here is a rough guideline to get a prototype going. You'll copy-paste some obj-c code from the sample, then create a bridging header, then start Node and use it from your Swift code.

These were copy-pasted:

//
//  NodeRunner.h
//

#ifndef NodeRunner_h
#define NodeRunner_h
#import <Foundation/Foundation.h>

@interface NodeRunner : NSObject {}
+ (void) startEngineWithArguments:(NSArray*)arguments;
@end

#endif
//
//  NodeRunner.mm
//

#include "NodeRunner.h"
#include <NodeMobile/NodeMobile.h>
#include <string>

@implementation NodeRunner

//node's libUV requires all arguments being on contiguous memory.
+ (void) startEngineWithArguments:(NSArray*)arguments
{
    int c_arguments_size=0;

    //Compute byte size need for all arguments in contiguous memory.
    for (id argElement in arguments)
    {
        c_arguments_size+=strlen([argElement UTF8String]);
        c_arguments_size++; // for '\0'
    }

    //Stores arguments in contiguous memory.
    char* args_buffer=(char*)calloc(c_arguments_size, sizeof(char));

    //argv to pass into node.
    char* argv[[arguments count]];

    //To iterate through the expected start position of each argument in args_buffer.
    char* current_args_position=args_buffer;

    //Argc
    int argument_count=0;

    //Populate the args_buffer and argv.
    for (id argElement in arguments)
    {
        const char* current_argument=[argElement UTF8String];

        //Copy current argument to its expected position in args_buffer
        strncpy(current_args_position, current_argument, strlen(current_argument));

        //Save current argument start position in argv and increment argc.
        argv[argument_count]=current_args_position;
        argument_count++;

        //Increment to the next argument's expected position.
        current_args_position+=strlen(current_args_position)+1;
    }

    //Start node, with argc and argv.
    node_start(argument_count,argv);
    free(args_buffer);
}
@end

ProjectName-Bridging-Header.h: If your project already has this file, just add a line, else add this file in your project root. Make sure in Xcode settings that this bridging file is being used during build:

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "NodeRunner.h"

Now in AppDelegate:

//
//  AppDelegate.swift
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // ... your other app code

        DispatchQueue.global(qos: .background).async {
            NodeRunner.startEngine(withArguments: ["node", "-e", "require('http').createServer((req, res) => res.end('alive!')).listen(3000);"])
        }

        return true
    }

  // ...
}

It would be better to rewrite the above to use NSThread, where you can give it the required 2MB stack size.

umangkathuria commented 4 years ago

@tomholub I'll give this a try! Thanks !!

umangkathuria commented 4 years ago

@tomholub Thanks for the initial setup! I am able to build the app using the steps you provided in Swift. Have you ever used this framework for calling specific functions? For example if there is a JavaScript file containing an API call that we'd like to invoke and then gather the response, how do we go about that? I dont see any documentation or methods around that. Even the startNode method just starts the node, how do we go about calling methods or passing value to javascript side through this framework?

Any help is really appreciated!!

Thanks!!

tomholub commented 4 years ago

You load the source file and pass it as argument during start to run your api.

                let jsFile = Bundle.main.path(forResource: "node-api.js", ofType: nil)!
                let jsFileSrc = try? String(contentsOfFile: jsFile)
                NodeRunner.startEngine(withArguments: ["node", "-e", jsFileSrc!])

Then you make http requests to the node with URLSession, Alamofire or whatever, at the port that you started the node api at. Same thing as https://code.janeasystems.com/nodejs-mobile/getting-started-ios at the bottom, just in swift.

umangkathuria commented 4 years ago

Yes, I was able to do this. But the challenge that I see with this method is that I cannot explicitly call a method. Let's say I have 10 utility functions which take an argument and return me a value. I have implemented a solution by using events in the javascript side. Although its working, but What I am trying to find is that is there a way which might be similar to what we have in JavaScriptCore. Something like this : "context.objectForKeyedSubscript("getData")?.call(withArguments: [])"

Where context is a JSContext Object and "getData" is the method in JavaScript that I am trying to call.

Do we have anything similar to this in the library?

Thank you for all your time and effort!! Appreciate it!

tomholub commented 4 years ago

You'd probably have to implement that using native node handlers, likely involving a bunch of c++, kind of like what react native is doing. This is not my domain.

rogeriochaves commented 4 years ago

In case anyone needs it with thread to increase stackSize like I did:

    @objc
    func startNode() {
        let jsFile = Bundle.main.path(forResource: "index.js", ofType: nil)!

        NodeRunner.startEngine(withArguments: ["node", jsFile])
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let nodejsThread = Thread(target:self, selector:#selector(startNode), object:nil)
        nodejsThread.stackSize = 2*1024*1024;
        nodejsThread.start()

        return true
    }
wltam-myndar commented 3 years ago

Hi guys, I ran into an issue that the swift project can only access a single .js file only, I could require() node functions like "http" and "querystring", but could not require any modules like the 'left-pad' from the sample, or a .js file under the same directory, it always return errors like below:

internal/modules/cjs/loader.js:834 throw err; ^

Error: Cannot find module 'left-pad' Require stack:

I've already run the "npm install" command under the "nodejs-project" and make sure "node_modules" folder have been created. Any ideas?

milochen0418 commented 3 years ago

@wltam-myndar About left-pad, you can refer https://www.theregister.com/2016/03/23/npm_left_pad_chaos/