OpenNHP Client Agent SDK Introduction

中文版


1 Client Agent SDK Introduction

1.1 Introduction

The OpenNHP Client Agent SDK is a standardized encapsulation of the OpenNHP Agent service. By integrating this SDK, applications can directly call the interface methods it provides to quickly achieve integration with OpenNHP. In different runtime environments, you only need to compile the SDK program into the corresponding system’s SDK file format:

Operating SystemDynamic Library File
Linuxnhp-agent.so
Windowsnhp-agent.dll
MacOSnhp-agent.dylib
Androidlibnhpagent.so
IOSnhpagent.xcframework

1.2 SDK Development

OpenNHP provides sample SDK source code. The samples include methods that might be used, such as initializing the agent, starting cyclic knocking, stopping cyclic knocking, single knock, canceling a single knock, adding nhp-server services, setting client user information, and key registration. SDK developers can directly compile the SDK source code samples provided in the OpenNHP project into the corresponding SDK files for direct invocation, or refer to the SDK source code samples to complete custom SDK development.

SDK Sample Source Code: opennhp/endpoints/agent/main/export.go

package main

/*
#include <stdlib.h>
*/
import "C"

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"unsafe"

	"github.com/OpenNHP/opennhp/endpoints/agent"
	"github.com/OpenNHP/opennhp/nhp/common"
	"github.com/OpenNHP/opennhp/nhp/core"
)

var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int

func deepCopyCString(c_str *C.char) string {
	if c_str == nil {
		return ""
	}
	goStr := C.GoString(c_str)
	return strings.Clone(goStr)
}

// Release the memory of the string buffer generated by NHPSDK.
//
//export nhp_free_cstring
func nhp_free_cstring(ptr *C.char) {
	C.free(unsafe.Pointer(ptr))
}

// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
//
//export nhp_agent_init
func nhp_agent_init(workingDir *C.char, logLevel C.int) bool {
	if gAgentInstance != nil {
		return true
	}

	gAgentInstance = &agent.UdpAgent{}
	err := gAgentInstance.Start(deepCopyCString(workingDir), int(logLevel))
	if err != nil {
		return false
	}

	return true
}

// Synchronously stop and release nhp_agent.
//
//export nhp_agent_close
func nhp_agent_close() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.Stop()
	gAgentInstance = nil
}

// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
//	(knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export nhp_agent_knockloop_start
func nhp_agent_knockloop_start() C.int {
	if gAgentInstance == nil {
		return -1
	}

	count := gAgentInstance.StartKnockLoop()
	return C.int(count)
}

// Synchronously stop the loop, knock-on sub thread.
//
//export nhp_agent_knockloop_stop
func nhp_agent_knockloop_stop() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.StopKnockLoop()
}

// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
//
//export nhp_agent_set_knock_user
func nhp_agent_set_knock_user(userId *C.char, devId *C.char, orgId *C.char, userData *C.char) bool {
	if gAgentInstance == nil {
		return false
	}
	jsonStr := deepCopyCString(userData)
	var data map[string]any
	if len(jsonStr) > 0 {
		err := json.Unmarshal([]byte(jsonStr), &data)
		if err != nil {
			return false
		}
	}

	gAgentInstance.SetDeviceId(deepCopyCString(devId))
	gAgentInstance.SetKnockUser(deepCopyCString(userId), deepCopyCString(orgId), data)
	return true
}

// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
//
//export nhp_agent_add_server
func nhp_agent_add_server(pubkey *C.char, ip *C.char, host *C.char, port C.int, expire int64) bool {
	if gAgentInstance == nil {
		return false
	}

	if pubkey == nil || (ip == nil && host == nil) {
		return false
	}

	serverPort := int(port)
	if serverPort == 0 {
		serverPort = 62206 // use default server listening port
	}

	serverPeer := &core.UdpPeer{
		Type:         core.NHP_SERVER,
		PubKeyBase64: deepCopyCString(pubkey),
		Ip:           deepCopyCString(ip),
		Port:         serverPort,
		Hostname:     deepCopyCString(host),
		ExpireTime:   expire,
	}
	gAgentInstance.AddServer(serverPeer)
	return true
}

// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
//
//export nhp_agent_remove_server
func nhp_agent_remove_server(pubkey *C.char) {
	if gAgentInstance == nil {
		return
	}
	if pubkey == nil {
		return
	}

	gAgentInstance.RemoveServer(deepCopyCString(pubkey))
}

// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
//
//export nhp_agent_add_resource
func nhp_agent_add_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
	if gAgentInstance == nil {
		return false
	}

	if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
		return false
	}

	resource := &agent.KnockResource{
		AuthServiceId:  deepCopyCString(aspId),
		ResourceId:     deepCopyCString(resId),
		ServerIp:       deepCopyCString(serverIp),
		ServerHostname: deepCopyCString(serverHostname),
		ServerPort:     int(serverPort),
	}
	err := gAgentInstance.AddResource(resource)
	return err == nil
}

// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
//
//export nhp_agent_remove_resource
func nhp_agent_remove_resource(aspId *C.char, resId *C.char) {
	if gAgentInstance == nil {
		return
	}

	if aspId == nil || resId == nil {
		return
	}

	gAgentInstance.RemoveResource(deepCopyCString(aspId), deepCopyCString(resId))
}

// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call nhp_agent_add_server before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling nhp_free_cstring to release the returned char* pointer
//
//export nhp_agent_knock_resource
func nhp_agent_knock_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) *C.char {
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			return
		}

		if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  deepCopyCString(aspId),
			ResourceId:     deepCopyCString(resId),
			ServerIp:       deepCopyCString(serverIp),
			ServerHostname: deepCopyCString(serverHostname),
			ServerPort:     int(serverPort),
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, _ = gAgentInstance.Knock(target)
	}()

	bytes, _ := json.Marshal(ackMsg)
	ret := C.CString(string(bytes))

	return ret
}

// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call nhp_agent_add_server before calling, to add the NHP server's public key, address, and other information to the agent.
//
//export nhp_agent_exit_resource
func nhp_agent_exit_resource(aspId *C.char, resId *C.char, serverIp *C.char, serverHostname *C.char, serverPort C.int) bool {
	var err error
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			err = common.ErrNoAgentInstance
			return
		}

		if aspId == nil || resId == nil || (serverIp == nil && serverHostname == nil) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			err = common.ErrInvalidInput
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  deepCopyCString(aspId),
			ResourceId:     deepCopyCString(resId),
			ServerIp:       deepCopyCString(serverIp),
			ServerHostname: deepCopyCString(serverHostname),
			ServerPort:     int(serverPort),
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			err = common.ErrKnockServerNotFound
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, err = gAgentInstance.ExitKnockRequest(target)
	}()

	return err == nil
}

// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_generate_keys
func nhp_generate_keys(cipherType C.int) *C.char {
	var e core.Ecdh
	switch core.EccTypeEnum(cipherType) {
	case core.ECC_SM2:
		e = core.NewECDH(core.ECC_SM2)
	case core.ECC_CURVE25519:
		fallthrough
	default:
		e = core.NewECDH(core.ECC_CURVE25519)
	}
	pub := e.PublicKeyBase64()
	priv := e.PrivateKeyBase64()

	res := fmt.Sprintf("%s|%s", priv, pub)
	pRes := C.CString(res)

	return pRes
}

// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export nhp_privkey_to_pubkey
func nhp_privkey_to_pubkey(cipherType C.int, privateBase64 *C.char) *C.char {
	privKey := deepCopyCString(privateBase64)
	privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
	if err != nil {
		return nil
	}

	e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
	if e == nil {
		return nil
	}
	pub := e.PublicKeyBase64()
	pPub := C.CString(pub)

	return pPub
}

2 Client Agent SDK Adaptation

2.1 Desktop SDK

2.1.1 Windows

2.1.1.1 Environment Preparation

Set up the compilation environment for Windows by referring to the Windows section in the System requirement chapter of Build OpenNHP Source Code.

2.1.1.2 Compiling the SDK
  • Method 1:Run the BAT file in the code root directory build.bat
    (Note: If an error occurs during the compilation process under windows, try this compilation method: In the Visual Studio developer command prompt for VS command window, switch to the project directory and execute the ./build.bat command)

  • Method 2: Command to compile the .dll file for the SDK separately:

    Navigate to the opennhp/endpoints/agent/main/ directory and execute:

    go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.dll main.go export.go

    (Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)

2.1.1.3 SDK Adaptation
  • java

    Java programs can call SDK methods using JNA:

    • OpennhpLibrary interface loads the OpenNHP agent SDK

      package org.example;
              
      import com.sun.jna.Library;
      import com.sun.jna.Native;
          
      /**
       * OpenNHP agent sdk interface
       *
       * @author haochangjiu
       * @version JDK 8
       * @className OpennhpLibrary
       * @date 2025/10/27
       */
      public interface OpennhpLibrary extends Library {
          // load OpenNHP agent sdk
          OpennhpLibrary INSTANCE = Native.load("nhp-agent", OpennhpLibrary.class);
          
          /**
           * @description Initialization of the nhp_agent instance working directory path:
           *              The configuration files to be read are located under workingdir/etc/,
           *              and log files will be generated under workingdir/logs/.
           * @param workingDir: the working directory path for the agent
           * @param logLevel:   0: silent, 1: error, 2: info, 3: debug, 4: verbose
           *                    return boolean Whether agent instance has been initialized successfully.
           * @return boolean
           * @author haochangjiu
           * @date 2025/10/27
           * {@link boolean}
           */
          boolean nhp_agent_init(String workingDir, int logLevel);
          
          /**
           * @description Synchronously stop and release nhp_agent.
           * @author haochangjiu
           * @date 2025/10/27
           */
          void nhp_agent_close();
          /**
           * @description Read the user information, resource information, server information,
           *              and other configuration files written under workingdir/etc,
           *              and asynchronously start the loop knocking thread.
           * @return int
           * @author haochangjiu
           * @date 2025/10/27
           * {@link int}
           */
          int nhp_agent_knockloop_start();
          
          /**
           * @description Synchronously stop the loop, knock-on sub thread
           * @author hangchangjiu
           * @date 2025/10/27
           */
          void nhp_agent_knockloop_stop();
      }
      
    • Application main entry, calling the SDK

      package org.example;
              
      import java.util.Scanner;
          
      /**
       * Application for calling the OpenNHP agent SDK
       *
       * @author haochangjiu
       * @version JDK 8
       * @className App
       * @date 2025/10/27
       */
      public class App {
          public static void main(String[] args) throws Exception {
              //        Initialize and start the OpenNHP agent SDK service
              boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init("D:\\console-workspace\\opennhp-knock", 3);
              if (!initFlag) {
                  System.out.println("NHP Agent init failed");
                  System.exit(0);
              }
              //        Invoke methods in the OpenNHP agent SDK via input commands
              Scanner scanner = new Scanner(System.in);
          
              while (true) {
                  System.out.print("> ");
                  if (scanner.hasNextLine()) {
                      String input = scanner.nextLine().trim();
                      if ("knock".equalsIgnoreCase(input)) {
                          System.out.println("start the loop knocking thread...");
                          OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start();
                      } else if ("cancel".equalsIgnoreCase(input)) {
                          System.out.println("stop the loop knocking thread...");
                          OpennhpLibrary.INSTANCE.nhp_agent_knockloop_stop();
                      } else if ("exit".equalsIgnoreCase(input)) {
                          System.out.println("exit nhp agent service...");
                          OpennhpLibrary.INSTANCE.nhp_agent_close();
                          break;
                      } else {
                          System.out.println("invalid input");
                      }
                  }
              }
              scanner.close();
          }
      }
      
  • c/c++

    C/C++ programs can refer to the sample SDK calling program in the project opennhp/endpoints/agent/sdkdemo/nhp-agent-demo.c to integrate the client agent SDK.

    #include <stdio.h>
    #include <unistd.h>
    #include "nhp-agent.h"
      
    int main() {
        // Initialize nhp_agent, only one nhp_agent singleton is allowed per process.
        nhp_agent_init(".", 3);
      
        // Set the user information for the knock-on-the-door feature.
        nhp_agent_set_knock_user("zengl", NULL, NULL, NULL);
      
        // Set NHP server information
        // If there is already a configuration file for the server, the call to nhp_agent_add_server can be omitted
        // Timestamp date is visible at https://unixtime.org/
        nhp_agent_add_server("replace_with_actual_publickeybase64", "192.168.1.66", NULL, 62206, 1748908471);
      
        // Send a request to the server to access the resource example/demo, and return information in the form of a JSON format string
        // Note: The resource information here is an independent input, and is unrelated to the resource information saved in the configuration file
        char *ret = nhp_agent_knock_resource("example", "demo", "192.168.1.66", NULL, 62206);
        printf("knock return: %s\n", ret);
      
        // Immediately close the agent's access to the example/demo resources,
        // if not invoked, access permission will automatically close after the door opening duration has passed.
        nhp_agent_exit_resource("example", "demo", "192.168.1.66", NULL, 62206);
      
        // Turn off and release nhp_agent.
        nhp_agent_close();
        return 0;
    }
    
  • python

    Use Python’s standard ctypes library to integrate the SDK.

    import ctypes
    from time import sleep
      
    # Windows
    nhp_agent = ctypes.CDLL('nhp-agent.dll')
    # Linux
    # mylib = ctypes.CDLL('./nhp-agent.so')
    # macOS
    # mylib = ctypes.CDLL('./nhp-agent.dylib')
      
    nhp_agent.nhp_agent_init.argtypes = [ctypes.c_char_p, ctypes.c_int]
    nhp_agent.nhp_agent_init.restype = ctypes.c_bool
      
    nhp_agent.nhp_agent_init.restype = ctypes.c_int
      
      
      
    if __name__ == '__main__':
        flag = nhp_agent.nhp_agent_init(ctypes.c_char_p(b"D:\\nhpagent"),3)
        if flag:
            print("nhp-agent init success")
        else:
            print("nhp-agent init failed")
        # start the loop knocking thread
        status = nhp_agent.nhp_agent_knockloop_start()
        if status >= 0:
            print("nhp-agent knockloop success")
            # Delay between calls
            sleep(30)
        else:
            print("nhp-agent knockloop failed")
      
        # stop nhp_agent
        nhp_agent.nhp_agent_close()
    
  • Other Languages

    Other development languages (C#, Rust, Go, Nodejs) can adapt the SDK according to their unique methods for calling SDK files. Among them, Go can also introduce the source code of the agent part from OpenNHP to adapt to OpenNHP without developing an SDK.

2.1.2 Linux

2.1.2.1 Environment Preparation

Set up the compilation environment for Linux by referring to the Linux section in the System requirement chapter of Build OpenNHP Source Code.

2.1.2.2 Compiling the SDK
  • Method 1: Run the script in the project root directory. make

  • Method 2: Command to compile the .so file for the SDK separately:

    Navigate to the opennhp/endpoints/agent/main/ directory and execute:

    go build -trimpath -buildmode=c-shared -ldflags '-s -w' -v -o nhp-agent.so main.go export.go

    (Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)

2.1.2.3 SDK Adaptation

The SDK adaptation on Linux is the same as on Windows. Refer to section 2.1.1.3 for the code.

(Note: Ensure the program can normally load the SDK’s .so file.)

2.1.3 MacOS

2.1.3.1 Environment Preparation

Set up the compilation environment for MacOS by referring to the MacOS section in the System requirement chapter of Build OpenNHP Source Code.

2.1.3.2 Compiling the SDK

The SDK compiled via the make command is a .so file, but the dynamic library file format on MacOS is .dylib. Therefore, SDK compilation needs to be done separately.

Navigate to the opennhp/endpoints/agent/main/ directory and execute the build command:

GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o nhp-agent.dylib main.go export.go

(Note: Because export.go does not contain a main method, main.go is included in the build command. For custom SDK code files that include a main method, the build command only needs the SDK code file and does not need to include main.go.)

2.1.3.3 SDK Adaptation

The SDK adaptation on MacOS is the same as on Windows. Refer to section 2.1.1.3 for the code.

(Note: Ensure the program can normally load the SDK’s .dylib file.)

2.2 Mobile SDK

2.2.1 Android

2.2.1.1 Environment Preparation
  • Compile the Android client agent SDK on Linux. Set up the compilation environment by referring to the Linux section in the System requirement chapter of Build OpenNHP Source Code.

  • Android NDK Environment:

    • Download and install Android NDK.

      wget https://dl.google.com/android/repository/android-ndk-r25b-linux.zip unzip android-ndk-r25b-linux.zip

    • Set environment variables.

      • Edit the bashrc file.

        vim ~/.bashrc

      • Add environment variables.

        # Set NDK path (according to your actual installation path)
        export ANDROID_NDK_HOME=/opt/android-ndk-r25b/
        export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
        # For arm64-v8a use aarch64 toolchain
        export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
        export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++
        
      • Make the configuration effective.

        source ~/.bashrc

2.2.1.2 Compiling the SDK

Navigate to the opennhp/endpoints/agent/main/ directory and execute the build command:

GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build -buildmode=c-shared -o libnhpagent.so main.go export.go

(Note: When an Android project loads .so files via JNA, it adds ‘lib’ to the front of the input .so file name. When compiling the SDK, the name should start with ‘lib’, e.g., libnhpagent.so.)

2.2.1.3 SDK Adaptation
  • Android Configuration (Applicable for both Kotlin and Java):

    • 1.Add the following configuration in build.gradle (app):

      Add under the android section:

      sourceSets {
          main {
              jniLibs.srcDirs = ['src/main/jniLibs', 'libs']
          }
      }
      

      Add the following dependencies under the dependencies section:

      // Note: It is recommended for Android to use an adapted JNA version, e.g., 5.13.0 or higher.implementation 'net.java.dev.jna:jna:5.13.0@aar'

      // Permission request framework: https://github.com/getActivity/XXPermissions implementation libs.xxpermissions

      In the libs.versions.toml file: Under [versions], add: xxpermissions = "18.6" Under [libraries], add: xxpermissions = { module = "com.github.getActivity:XXPermissions", version.ref = "xxpermissions" }

    • 2.Add file storage read and write permissions in the AndroidManifest.xml file:

      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
      <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
      <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
      <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
      
  • Kotlin

    Kotlin Sample Code for Android Application SDK Adaptation

    package com.example.androidtestsoapp
      
    import android.os.Bundle
    import android.os.Environment
    import android.util.Log
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.padding
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.tooling.preview.Preview
    import com.example.androidtestsoapp.ui.theme.AndroidTestSoAppTheme
    import com.hjq.permissions.Permission
    import com.hjq.permissions.XXPermissions
    import java.io.File
      
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            setContent {
                AndroidTestSoAppTheme {
                    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                        Greeting(
                            name = "Android",
                            modifier = Modifier.padding(innerPadding)
                        )
                    }
                }
            }
            // Request permissions - read/write
            XXPermissions.with(this)
                .permission(Permission.WRITE_EXTERNAL_STORAGE)
                .permission(Permission.READ_MEDIA_IMAGES)
                .permission(Permission.READ_MEDIA_VIDEO)
                .permission(Permission.READ_MEDIA_AUDIO)
                .request { permissions, allGranted ->
                    if (allGranted) {
                        Log.d("MainActivity", "Permissions granted")
                        performFileOperations()
                    } else {
                        Log.d("MainActivity", "Permissions not granted")
                    }
                }
        }
    }
      
    /**
     * Need to place the nhp folder containing the etc folder in the phone's download folder
     * After reading the phone storage download directory, call OpennhpLibrary
     */
    private fun performFileOperations() {
        // Read phone storage download directory
        val appDir = Environment.getExternalStorageDirectory().toString() + File.separator + "download"
        // Check if zero folder exists in download
        val file = File(appDir)
        if (!file.exists()) {
            Log.d("MainActivity", "Download folder does not exist")
            return
        }
        Log.d("MainActivity", "Download folder exists")
        val appDir1 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp"
        // Check if nhp folder exists in download
        val file1 = File(appDir1)
        if (!file1.exists()) {
            Log.d("MainActivity", "nhp folder does not exist")
            return
        }
        val appDir2 = Environment.getExternalStorageDirectory().toString() + File.separator + "download" + File.separator + "nhp"+ File.separator + "etc"
        // Check if etc folder exists in download
        val file2 = File(appDir2)
        if (!file2.exists()) {
            Log.d("MainActivity", "Etc folder does not exist")
            return
        }
      
        val initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 2)
        if (!initFlag) {
            println("NHP Agent init failed")
            return
        }
        println("start the loop knocking thread...")
        val flag:Int = OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start()
        // Print result
        if (flag > 0) {
            println("NHP Agent knockloop start success")
        } else {
            println("NHP Agent knockloop start failed")
        }
    }
      
    @Composable
    fun Greeting(name: String, modifier: Modifier = Modifier) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
      
    @Preview(showBackground = true)
    @Composable
    fun GreetingPreview() {
        AndroidTestSoAppTheme {
            Greeting("Android")
        }
    }
    
  • java

    • Create the OpennhpLibrary interface to load the OpenNHP agent SDK.

      (Note: When introducing .so files in an Android project, ‘lib’ is added before the dynamic library file name. That is, the SDK name loaded in the code is ‘nhpagent’, but the actual SDK loaded by the program is the ‘libnhpagent.so’ file.)

      package org.example;
          
      import com.sun.jna.Library;
      import com.sun.jna.Native;
          
      /**
       * OpenNHP agent sdk interface
       *
       * @author haochangjiu
       * @version JDK 8
       * @className OpennhpLibrary
       * @date 2025/10/27
       */
      public interface OpennhpLibrary extends Library {
          // load OpenNHP agent sdk
          OpennhpLibrary INSTANCE = Native.load("nhpagent", OpennhpLibrary.class);
          
          /**
           * @description Initialization of the nhp_agent instance working directory path:
           *              The configuration files to be read are located under workingdir/etc/,
           *              and log files will be generated under workingdir/logs/.
           * @param workingDir: the working directory path for the agent
           * @param logLevel:   0: silent, 1: error, 2: info, 3: debug, 4: verbose
           *                    return boolean Whether agent instance has been initialized successfully.
           * @return boolean
           * @author haochangjiu
           * @date 2025/10/27
           * {@link boolean}
           */
          boolean nhp_agent_init(String workingDir, int logLevel);
          
          /**
           * @description Synchronously stop and release nhp_agent.
           * @author haochangjiu
           * @date 2025/10/27
           */
          void nhp_agent_close();
          /**
           * @description Read the user information, resource information, server information,
           *              and other configuration files written under workingdir/etc,
           *              and asynchronously start the loop knocking thread.
           * @return int
           * @author haochangjiu
           * @date 2025/10/27
           * {@link int}
           */
          int nhp_agent_knockloop_start();
          
          /**
           * @description Synchronously stop the loop, knock-on sub thread
           * @author hangchangjiu
           * @date 2025/10/27
           */
          void nhp_agent_knockloop_stop();
      }
      
    • Calling the SDK: In the sample, the configuration file etc folder is placed in the nhp directory under the phone’s download directory.

      package org.example;
          
      import android.os.Bundle;
      import android.os.Environment;
      import android.util.Log;
      import androidx.appcompat.app.AppCompatActivity;
          
          
      import com.OpennhpLibrary;
      import com.fancy.zerotrust.R;
          
      import java.io.File;
          
      public class MainActivity extends AppCompatActivity {
          
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              // Read the phone's storage download directory.
              String appDir = Environment.getExternalStorageDirectory() + File.separator + "download";
              // Does the nhp directory exist in the downloads
              File file = new File(appDir);
              if (!file.exists()) {
                  Log.d("MainActivity","download file not exist!");
                  return;
              }
              Log.d("MainActivity","download file exist!");
              String appDir1 = Environment.getExternalStorageDirectory() + File.separator + "download"+ File.separator + "nhp";
              boolean initFlag = OpennhpLibrary.INSTANCE.nhp_agent_init(appDir1, 3);
              if (!initFlag) {
                  System.out.println("NHP Agent init failed");
                  System.exit(0);
              }
              System.out.println("start the loop knocking thread...");
              OpennhpLibrary.INSTANCE.nhp_agent_knockloop_start();
          }
      }
      

2.2.2 IOS

2.2.2.1 Environment Preparation
  • Compile the IOS client agent SDK on MacOS. Set up the compilation environment by referring to the MacOS section in the System requirement chapter of Build OpenNHP Source Code.

  • Ensure Xcode is installed. If not, install it from the App Store.

  • Install gomobile:

    • Install

      go install golang.org/x/mobile/cmd/gomobile@latest

    • Initialize

      gomobile init

2.2.2.2 SDK Sample

When compiling the .xcframework file required for IOS, the names of the exported methods must start with a capital letter, and the parameter types must be standard Go language types, not C.int and C.char. Another important point is that the code cannot be under package main. Move the program to a newly created sdk path.

Modified code based on the export.go file in OpenNHP is as follows:

package sdk

import "C"
import (
	"encoding/base64"
	"encoding/json"
	"fmt"

	"github.com/OpenNHP/opennhp/endpoints/agent"
	"github.com/OpenNHP/opennhp/nhp/common"
	"github.com/OpenNHP/opennhp/nhp/core"
)

var gAgentInstance *agent.UdpAgent
var gWorkingDir string
var gLogLevel int

// Initialization of the nhp_agent instance working directory path:
// The configuration files to be read are located under workingdir/etc/,
// and log files will be generated under workingdir/logs/.
//
// Input:
// workingDir: the working directory path for the agent
// logLevel: 0: silent, 1: error, 2: info, 3: debug, 4: verbose
//
// Return:
// Whether agent instance has been initialized successfully.
func NhpAgentInit(workingDir string, logLevel int) bool {
	if gAgentInstance != nil {
		return true
	}

	gAgentInstance = &agent.UdpAgent{}
	err := gAgentInstance.Start(workingDir, logLevel)
	if err != nil {
		return false
	}

	return true
}

// Synchronously stop and release nhp_
func NhpAgentClose() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.Stop()
	gAgentInstance = nil
}

// Read the user information, resource information, server information,
// and other configuration files written under workingdir/etc,
// and asynchronously start the loop knocking thread.
//
// Input: None
//
// Return:
// -1: Uninitialized error
// >=0: The number of resources requested to knock by the knocking thread at the time of the call
//
//	(knocking resources will be synchronized with changes in the configuration in workingdir/etc/resource.toml).
//
//export NhpAgentKnockloopStart
func NhpAgentKnockloopStart() int {
	if gAgentInstance == nil {
		return -1
	}

	count := gAgentInstance.StartKnockLoop()
	return count
}

// Synchronously stop the loop, knock-on sub thread.
func NhpAgentKnockloopStop() {
	if gAgentInstance == nil {
		return
	}

	gAgentInstance.StopKnockLoop()
}

// Setting agent's represented user information
//
// Input:
// userId: User identification (optional, but not recommended to be empty)
// devId: Device identification (optional)
// orgId: Organization or company identification (optional)
// userData: Additional fields required to interface with backend services (json format string, optional)
//
// Return:
// Whether the user information is set successfully
func NhpAgentSetKnockUser(userId string, devId string, orgId string, userData string) bool {
	if gAgentInstance == nil {
		return false
	}
	var data map[string]any
	if len(userData) > 0 {
		err := json.Unmarshal([]byte(userData), &data)
		if err != nil {
			return false
		}
	}

	gAgentInstance.SetDeviceId(devId)
	gAgentInstance.SetKnockUser(userId, orgId, data)
	return true
}

// Add an NHP server information to the agent for use in knocking on the door
// (the agent can initiate different knocking requests to multiple NHP servers).
//
// Input:
// pubkey: Public key of the NHP server
// ip: IP address of the NHP server
// host: Domain name of the NHP server (if a domain name is set, the ip item is optional)
// port: Port number for the NHP server to operate (if set to 0, the default port 62206 will be used)
// expire: Expiration time of the NHP server's public key (in epoch seconds, set to 0 for permanent)
//
// Return:
// Whether the server information has been successfully added.
func NhpAgentAddServer(pubkey string, ip string, host string, port int, expire int64) bool {
	if gAgentInstance == nil {
		return false
	}

	if len(pubkey) == 0 || (len(ip) == 0 && len(host) == 0) {
		return false
	}

	serverPort := int(port)
	if serverPort == 0 {
		serverPort = 62206 // use default server listening port
	}

	serverPeer := &core.UdpPeer{
		Type:         core.NHP_SERVER,
		PubKeyBase64: pubkey,
		Ip:           ip,
		Port:         serverPort,
		Hostname:     host,
		ExpireTime:   expire,
	}
	gAgentInstance.AddServer(serverPeer)
	return true
}

// Delete NHP server information from the agent
//
// Input:
// pubkey: NHP server public key
func NhpAgentRemoveServer(pubkey string) {
	if gAgentInstance == nil {
		return
	}
	if len(pubkey) == 0 {
		return
	}

	gAgentInstance.RemoveServer(pubkey)
}

// Please add a resource information for the agent to use for knocking on the door
// (the agent can initiate a knock-on request for different resources)
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the resource information has been added successfully
func NhpAgentAddResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
	if gAgentInstance == nil {
		return false
	}

	if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
		return false
	}

	resource := &agent.KnockResource{
		AuthServiceId:  aspId,
		ResourceId:     resId,
		ServerIp:       serverIp,
		ServerHostname: serverHostname,
		ServerPort:     serverPort,
	}
	err := gAgentInstance.AddResource(resource)
	return err == nil
}

// Delete resource information from the agent
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
func NhpAgentRemoveResource(aspId string, resId string) {
	if gAgentInstance == nil {
		return
	}

	if len(aspId) == 0 || len(resId) == 0 {
		return
	}

	gAgentInstance.RemoveResource(aspId, resId)
}

// The agent initiates a single knock on the door request to the server hosting the resource
//
// Input:
// aspId: Authentication service provider identifier
// resId: Resource identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Returns:
// The server's response message (json format string buffer pointer):
// "errCode": Error code (string, "0" indicates success)
// "errMsg": Error message (string)
// "resHost": Resource server address ("resHost": {"Server Name 1":"Server Hostname 1", "Server Name 2":"Server Hostname 2", ...})
// "opnTime": Door opening duration (integer, in seconds)
// "aspToken": Token generated after authentication by the ASP (optional)
// "agentAddr": Agent's IP address from the perspective of the NHP server
// "preActs": Pre-connection information related to the resource (optional)
// "redirectUrl": HTTP redirection link (optional)
//
// It is necessary to call NhpAgentAddServer before calling,
// to add the NHP server's public key, address, and other information to the agent
// The caller is responsible for calling NhpFreeCstring to release the returned char* pointer
func NhpAgentKnockResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) string {
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			return
		}

		if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  aspId,
			ResourceId:     resId,
			ServerIp:       serverIp,
			ServerHostname: serverHostname,
			ServerPort:     serverPort,
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, _ = gAgentInstance.Knock(target)
	}()

	bytes, _ := json.Marshal(ackMsg)

	return string(bytes)
}

// The agent explicitly informs the NHP server to exit its access permission to the resource.
//
// Input:
// aspId: Authentication Service Provider Identifier
// resId: Resource Identifier
// serverIp: NHP server IP address or domain name (the NHP server managing the resource)
// serverHostname: NHP server domain name (the NHP server managing the resource)
// serverPort: NHP server port (the NHP server managing the resource)
//
// Return:
// Whether the exit was successful
//
// It is necessary to call NhpAgentAddServer before calling, to add the NHP server's public key, address, and other information to the
func NhpAgentExitResource(aspId string, resId string, serverIp string, serverHostname string, serverPort int) bool {
	var err error
	ackMsg := &common.ServerKnockAckMsg{}

	func() {
		if gAgentInstance == nil {
			ackMsg.ErrCode = common.ErrNoAgentInstance.ErrorCode()
			ackMsg.ErrMsg = common.ErrNoAgentInstance.Error()
			err = common.ErrNoAgentInstance
			return
		}

		if len(aspId) == 0 || len(resId) == 0 || (len(serverIp) == 0 && len(serverHostname) == 0) {
			ackMsg.ErrCode = common.ErrInvalidInput.ErrorCode()
			ackMsg.ErrMsg = common.ErrInvalidInput.Error()
			err = common.ErrInvalidInput
			return
		}

		resource := &agent.KnockResource{
			AuthServiceId:  aspId,
			ResourceId:     resId,
			ServerIp:       serverIp,
			ServerHostname: serverHostname,
			ServerPort:     serverPort,
		}

		peer := gAgentInstance.FindServerPeerFromResource(resource)
		if peer == nil {
			ackMsg.ErrCode = common.ErrKnockServerNotFound.ErrorCode()
			ackMsg.ErrMsg = common.ErrKnockServerNotFound.Error()
			err = common.ErrKnockServerNotFound
			return
		}

		target := &agent.KnockTarget{
			KnockResource: *resource,
			ServerPeer:    peer,
		}

		ackMsg, err = gAgentInstance.ExitKnockRequest(target)
	}()

	return err == nil
}

// cipherType: 0-curve25519; 1-sm2
// result: "privatekey"|"publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpGenerateKeys
func NhpGenerateKeys(cipherType int) string {
	var e core.Ecdh
	switch core.EccTypeEnum(cipherType) {
	case core.ECC_SM2:
		e = core.NewECDH(core.ECC_SM2)
	case core.ECC_CURVE25519:
		fallthrough
	default:
		e = core.NewECDH(core.ECC_CURVE25519)
	}
	pub := e.PublicKeyBase64()
	priv := e.PrivateKeyBase64()

	res := fmt.Sprintf("%s|%s", priv, pub)

	return res
}

// cipherType: 0-curve25519; 1-sm2
// privateBase64: private key in base64 format
// result: "publickey"
// caller is responsible to free the returned char* pointer
//
//export NhpPrivkeyToPubkey
func NhpPrivkeyToPubkey(cipherType int, privateBase64 string) string {
	privKey := privateBase64
	privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
	if err != nil {
		return ""
	}

	e := core.ECDHFromKey(core.EccTypeEnum(cipherType), privKeyBytes)
	if e == nil {
		return ""
	}
	pub := e.PublicKeyBase64()

	return pub
}
2.2.2.3 Compiling the SDK

Navigate to the opennhp/endpoints/agent/sdk/ directory and execute the build command.(Note: The re-edited sdk source code files are placed under opennhp/endpoints/agent/sdk/)

gomobile bind -target ios -o nhpagent.xcframework .

2.2.2.4 SDK Adaptation
  • Objective-C
    • FileCopyManager.h: Declares methods to copy SDK configuration files to the sandbox.
      //
      //  FileCopyManager.h
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import <Foundation/Foundation.h>
          
      NS_ASSUME_NONNULL_BEGIN
          
      @interface FileCopyManager : NSObject
      /// Copy the specified file(s) to the etc and certs directories in the application's home directory
      + (void)copyFilesToSandboxEtc;
      @end
          
      NS_ASSUME_NONNULL_END
      
    • FileCopyManager.m: Implementation of FileCopyManager.h.
      //
      //  FileCopyManager.m
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import "FileCopyManager.h"
      #import <Foundation/Foundation.h>
          
      @implementation FileCopyManager
          
      /// Copy the specified file(s) to the etc and certs directories in the application's home directory
      + (void)copyFilesToSandboxEtc {
          // 1. Retrieve the sandboxed Documents directory
          NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
          NSURL *documentsURL = [documentsURLs firstObject];
          if (!documentsURL) {
              NSLog(@"Failed to retrieve Documents directory");
              return;
          }
              
          // 2. Define paths for etc and certs directories within the sandbox
          NSURL *etcURL = [documentsURL URLByAppendingPathComponent:@"etc"];
          NSURL *certsURL = [etcURL URLByAppendingPathComponent:@"certs"];
              
          // 3. Create etc and certs directories (if they don't exist)
          [self createDirectoryIfNotExists:etcURL];
          [self createDirectoryIfNotExists:certsURL];
              
          // 4. Copy toml files to the etc directory
          NSArray *tomlFiles = @[@"server.toml", @"config.toml", @"dhp.toml", @"resource.toml"];
          for (NSString *fileName in tomlFiles) {
              [self copyFileFromBundle:fileName toDestinationURL:etcURL];
          }
              
          // 5. Copy certificate files to the etc/certs directory
          NSArray *certFiles = @[@"server.crt", @"server.key"];
          for (NSString *fileName in certFiles) {
              [self copyFileFromBundle:fileName toDestinationURL:certsURL];
          }
      }
          
      /// Create directory if it does not exist
      + (void)createDirectoryIfNotExists:(NSURL *)directoryURL {
          NSFileManager *fileManager = [NSFileManager defaultManager];
          if (![fileManager fileExistsAtPath:directoryURL.path]) {
              NSError *error;
              BOOL success = [fileManager createDirectoryAtURL:directoryURL
                                    withIntermediateDirectories:YES
                                                     attributes:nil
                                                          error:&error];
              if (success) {
                  NSLog(@"Directory created successfully: %@", directoryURL.path);
              } else {
                  NSLog(@"Failed to create directory: %@, error: %@", directoryURL.path, error.localizedDescription);
              }
          } else {
              NSLog(@"Directory already exists: %@", directoryURL.path);
          }
      }
          
      /// Copy file from Bundle to destination path
      + (void)copyFileFromBundle:(NSString *)fileName toDestinationURL:(NSURL *)destinationURL {
          // Get the file path in the Bundle
          NSURL *sourceURL = [[NSBundle mainBundle] URLForResource:[fileName stringByDeletingPathExtension]
                                                      withExtension:[fileName pathExtension]];
          if (!sourceURL) {
              NSLog(@"File not found in Bundle: %@", fileName);
              return;
          }
              
          // Destination file path (destination directory + file name)
          NSURL *destFileURL = [destinationURL URLByAppendingPathComponent:fileName];
              
          // Copy file (if it doesn't exist)
          NSFileManager *fileManager = [NSFileManager defaultManager];
          if (![fileManager fileExistsAtPath:destFileURL.path]) {
              NSError *error;
              BOOL success = [fileManager copyItemAtURL:sourceURL toURL:destFileURL error:&error];
              if (success) {
                  NSLog(@"File copied successfully: %@ -> %@", fileName, destFileURL.path);
              } else {
                  NSLog(@"File copy failed: %@, error: %@", fileName, error.localizedDescription);
              }
          } else {
              NSLog(@"File already exists: %@", destFileURL.path);
          }
      }
          
      @end
      
    • ViewController.m: Program main entry, calling SDK methods.
      //
      //  ViewController.m
      //  TestXCFramework
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      #import "ViewController.h"
      #import <Nhpagent/Nhpagent.h>
      #import "FileCopyManager.h"
          
      @interface ViewController ()
          
      @end
          
      @implementation ViewController
          
      - (void)viewDidLoad {
          [super viewDidLoad];
          // Do any additional setup after loading the view.
          // Invoke method to copy files from etc folder to sandbox etc directory
          [FileCopyManager copyFilesToSandboxEtc];
          // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder
          NSArray *documentsURLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
          NSURL *documentsURL = [documentsURLs firstObject];
          if (!documentsURL) {
              NSLog(@"Error: Failed to read Documents directory");
          }
          // Get the parent directory path of the etc folder
          NSString *etcPath = documentsURL.path;
          // SdkNhpAgentInit
          BOOL initFlag = SdkNhpAgentInit(etcPath, 3);
          if (!initFlag) {
              NSLog(@"NHP Agent init failed");
              return;
          }
          // knockloop_start
          long value = SdkNhpAgentKnockloopStart();
          NSLog(@"SdkNhpAgentKnockloopStart value : %ld", value);
      }
          
      @end
      
  • Swift
    • FileCopyManager.swift: Methods to copy SDK configuration files to the sandbox.

      //
      //  FileCopyManager.swift
      //  TestXCFrameworkSwift
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      import UIKit
      import Foundation
          
      class FileCopyManager {
              
          /// Copy specified files to the etc and certs directories in the sandbox
          static func copyFilesToSandboxEtc() {
              // 1. Get the Documents directory in the sandbox
              guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
                  print("Failed to get Documents directory")
                  return
              }
                  
              // 2. Define paths for etc and certs directories in the sandbox
              let etcURL = documentsURL.appendingPathComponent("etc")
              let certsURL = etcURL.appendingPathComponent("certs")
                  
              // 3. Create etc and certs directories (if they don't exist)
              createDirectoryIfNotExists(at: etcURL)
              createDirectoryIfNotExists(at: certsURL)
                  
              // 4. Copy toml files to the etc directory
              let tomlFiles = ["server.toml", "config.toml", "dhp.toml", "resource.toml"]
              tomlFiles.forEach { fileName in
                  copyFileFromBundle(fileName: fileName, to: etcURL)
              }
                  
              // 5. Copy certificate files to the etc/certs directory
              let certFiles = ["server.crt", "server.key"]
              certFiles.forEach { fileName in
                  copyFileFromBundle(fileName: fileName, to: certsURL)
              }
          }
              
          /// Create directory if it doesn't exist
          private static func createDirectoryIfNotExists(at url: URL) {
              let fileManager = FileManager.default
              guard !fileManager.fileExists(atPath: url.path) else {
                  print("Directory already exists: \(url.path)")
                  return
              }
                  
              do {
                  try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
                  print("Directory created successfully: \(url.path)")
              } catch {
                  print("Failed to create directory: \(url.path), error: \(error.localizedDescription)")
              }
          }
              
          /// Copy file from Bundle to destination path
          private static func copyFileFromBundle(fileName: String, to destinationURL: URL) {
              // Split filename and extension (handling files with extensions)
              let fileNameWithoutExt = (fileName as NSString).deletingPathExtension
              let fileExt = (fileName as NSString).pathExtension
                  
              // Get the file path in the Bundle
              guard let sourceURL = Bundle.main.url(forResource: fileNameWithoutExt, withExtension: fileExt) else {
                  print("File not found in Bundle: \(fileName)")
                  return
              }
                  
              // Destination file path (destination directory + filename)
              let destFileURL = destinationURL.appendingPathComponent(fileName)
              let fileManager = FileManager.default
                  
              // Copy file (if it doesn't exist)
              guard !fileManager.fileExists(atPath: destFileURL.path) else {
                  print("File already exists: \(destFileURL.path)")
                  return
              }
                  
              do {
                  try fileManager.copyItem(at: sourceURL, to: destFileURL)
                  print("File copied successfully: \(fileName) -> \(destFileURL.path)")
              } catch {
                  print("File copy failed: \(fileName), error: \(error.localizedDescription)")
              }
          }
      }
      
    • ViewController.swift: Program main entry, calling SDK methods.

      //
      //  ViewController.swift
      //  TestXCFrameworkSwift
      //
      //  Created by haochangjiu on 2025/10/30.
      //
          
      import UIKit
      import Nhpagent
          
      class ViewController: UIViewController {
          override func viewDidLoad() {
              super.viewDidLoad()
              // Do any additional setup after loading the view.
              // Call method to copy files from etc folder to sandbox etc directory
              FileCopyManager.copyFilesToSandboxEtc()
              // Retrieve the sandbox target path (Documents), which is the parent directory of the etc folder
              guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
                  print("Error: Failed to read Documents directory")
                  return
              }
              // Get the parent directory path of the etc folder
              let etcPath: String = documentsURL.path
              // Call SdkNhpAgentInit for initialization
              let initFlag: Bool = SdkNhpAgentInit(etcPath, 3)
              if !initFlag {
                  print("NHP Agent init failed")
              }
              // Call knockloop_start
              let value = SdkNhpAgentKnockloopStart()
              print("SdkNhpAgentKnockloopStart value: %ld", value)
        }
      }